Files
2026-09-04 20:56:18 +00:00

95 lines
2.4 KiB
Python

"""Build and persist Firecracker configuration for one VM."""
from __future__ import annotations
import json
import math
import os
import tempfile
from collections.abc import Mapping
from ipaddress import IPv4Address, IPv4Network
from pathlib import Path
from typing import Any
from ..config import Settings
from ..domain import VmRecord
def vcpu_count(cpu: float) -> int:
"""Firecracker accepts whole vCPUs; fractional capacity rounds up for now."""
return max(1, math.ceil(cpu))
def boot_ip_argument(
guest_ip: IPv4Address,
gateway: IPv4Address,
network: IPv4Network,
) -> str:
return f"ip={guest_ip}::{gateway}:{network.netmask}::eth0:off"
def build_config(
settings: Settings,
vm: VmRecord,
kernel: Path,
rootfs: Path,
) -> dict[str, Any]:
boot_args = " ".join(
(
"console=ttyS0",
"reboot=k",
"panic=1",
"pci=off",
boot_ip_argument(IPv4Address(vm.guest_ip), settings.gateway, settings.network),
)
)
return {
"boot-source": {
"kernel_image_path": str(kernel),
"boot_args": boot_args,
},
"drives": [
{
"drive_id": "rootfs",
"path_on_host": str(rootfs),
"is_root_device": True,
"is_read_only": False,
}
],
"machine-config": {
"vcpu_count": vcpu_count(vm.cpu),
"mem_size_mib": vm.ram_mib,
},
"network-interfaces": [
{
"iface_id": "eth0",
"guest_mac": vm.mac,
"host_dev_name": vm.tap,
}
],
}
def write_config(path: Path, config: Mapping[str, Any]) -> None:
"""Atomically publish a rendered Firecracker configuration file."""
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{path.name}.",
suffix=".tmp",
dir=path.parent,
)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as temporary_file:
json.dump(config, temporary_file, indent=2)
temporary_file.write("\n")
temporary_file.flush()
os.fsync(temporary_file.fileno())
os.replace(temporary_name, path)
finally:
try:
os.unlink(temporary_name)
except FileNotFoundError:
pass