83 lines
3.0 KiB
Python
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}"
|
|
)
|