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