Files
nest/uvm/firecracker/api.py
T
2026-09-04 20:56:18 +00:00

83 lines
3.0 KiB
Python

"""Typed Firecracker HTTP requests over a per-VM Unix-domain socket."""
from __future__ import annotations
import http.client
import json
import socket
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from ..errors import FirecrackerError
class _UnixHTTPConnection(http.client.HTTPConnection):
def __init__(self, socket_path: Path, timeout: float) -> None:
super().__init__("localhost", timeout=timeout)
self._socket_path = socket_path
def connect(self) -> None:
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.connect(str(self._socket_path))
class FirecrackerClient:
"""Keep Firecracker endpoint details out of lifecycle orchestration."""
def __init__(self, socket_path: Path, timeout_s: float) -> None:
self._socket_path = socket_path
self._timeout_s = timeout_s
def configure_and_start(self, config: Mapping[str, Any]) -> None:
machine = config["machine-config"]
boot = config["boot-source"]
drives = config["drives"]
interfaces = config["network-interfaces"]
if not isinstance(machine, Mapping) or not isinstance(boot, Mapping):
raise FirecrackerError("invalid Firecracker configuration")
if not isinstance(drives, list) or not drives:
raise FirecrackerError("Firecracker configuration has no root drive")
if not isinstance(interfaces, list) or not interfaces:
raise FirecrackerError("Firecracker configuration has no network interface")
self._request(
"PUT",
"/machine-config",
{
"vcpu_count": machine["vcpu_count"],
"mem_size_mib": machine["mem_size_mib"],
"smt": False,
},
)
self._request("PUT", "/boot-source", boot)
self._request("PUT", "/drives/rootfs", drives[0])
self._request("PUT", "/network-interfaces/eth0", interfaces[0])
self._request("PUT", "/actions", {"action_type": "InstanceStart"})
def _request(self, method: str, path: str, body: object) -> None:
payload = json.dumps(body).encode("utf-8")
connection = _UnixHTTPConnection(self._socket_path, self._timeout_s)
try:
connection.request(
method,
path,
payload,
{
"Content-Type": "application/json",
"Accept": "application/json",
},
)
response = connection.getresponse()
response_body = response.read().decode("utf-8", errors="replace")
except (OSError, http.client.HTTPException) as error:
raise FirecrackerError(f"Firecracker API {method} {path}: {error}") from error
finally:
connection.close()
if response.status >= 300:
raise FirecrackerError(
f"Firecracker API {method} {path}: {response.status} {response_body}"
)