"""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}" )