This commit is contained in:
its.kstyagi@gmail.com
2026-09-04 20:56:18 +00:00
commit 1022f24c34
43 changed files with 6160 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for the local uvm command-line application."""
+160
View File
@@ -0,0 +1,160 @@
from __future__ import annotations
import argparse
import io
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from uvm.cli import _run_command
from uvm.domain import VmRecord
from uvm.images import GuestAssets
def make_vm() -> VmRecord:
return VmRecord(
id="vm-test",
cpu=1,
ram_mib=512,
guest_ip="10.42.0.2",
gateway="10.42.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/firecracker.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
username="admin",
password="stored-secret",
)
class CliSshTests(unittest.TestCase):
def setUp(self) -> None:
self.vm = make_vm()
self.application = SimpleNamespace(
lifecycle=SimpleNamespace(find_for_ssh=lambda _identifier: self.vm)
)
def test_ssh_uses_a_stable_vm_host_key_alias_by_default(self) -> None:
args = argparse.Namespace(
command="ssh",
vm=self.vm.id,
key=None,
user="root",
insecure_host_key=False,
)
with patch("uvm.cli.os.execvp") as execvp:
_run_command(self.application, args)
command = execvp.call_args.args[1]
self.assertIn("HostKeyAlias=uvm-vm-test", command)
self.assertIn("StrictHostKeyChecking=accept-new", command)
self.assertNotIn("StrictHostKeyChecking=no", command)
def test_ssh_only_disables_verification_when_explicitly_requested(self) -> None:
args = argparse.Namespace(
command="ssh",
vm=self.vm.id,
key=None,
user="root",
insecure_host_key=True,
)
with patch("uvm.cli.os.execvp") as execvp:
_run_command(self.application, args)
command = execvp.call_args.args[1]
self.assertIn("StrictHostKeyChecking=no", command)
self.assertNotIn("HostKeyAlias=uvm-vm-test", command)
def test_ssh_uses_the_username_stored_for_the_vm(self) -> None:
args = argparse.Namespace(
command="ssh",
vm=self.vm.id,
key=None,
user=None,
insecure_host_key=False,
)
with patch("uvm.cli.os.execvp") as execvp:
_run_command(self.application, args)
self.assertEqual(execvp.call_args.args[1][-1], "admin@10.42.0.2")
class CliCreateTests(unittest.TestCase):
def test_create_passes_credentials_without_printing_the_password(self) -> None:
captured = None
def create(spec):
nonlocal captured
captured = spec
vm = make_vm()
vm.username = spec.username
vm.password = spec.password
return vm
application = SimpleNamespace(lifecycle=SimpleNamespace(create=create))
args = argparse.Namespace(
command="create",
cpu="1",
ram="512",
host_ip=None,
username="admin",
password="custom-secret",
)
output = io.StringIO()
with redirect_stdout(output):
result = _run_command(application, args)
self.assertEqual(result, 0)
self.assertEqual(captured.username, "admin")
self.assertEqual(captured.password, "custom-secret")
self.assertNotIn("custom-secret", output.getvalue())
class CliInstallTests(unittest.TestCase):
def test_install_next_step_does_not_assume_an_installed_console_command(self) -> None:
state_store = SimpleNamespace(initialize=lambda: None)
application = SimpleNamespace(
settings=SimpleNamespace(allow_unverified_downloads=False),
installer=SimpleNamespace(
install=lambda **_kwargs: (
Path("/tmp/firecracker"),
GuestAssets(kernel=Path("/tmp/vmlinux"), rootfs=Path("/tmp/rootfs")),
)
),
state_store=state_store,
)
args = argparse.Namespace(command="install", force=False)
output = io.StringIO()
with redirect_stdout(output):
result = _run_command(application, args)
self.assertEqual(result, 0)
self.assertIn("Run your UVM command with: create --cpu 1 --ram 512", output.getvalue())
self.assertNotIn("sudo uvm create", output.getvalue())
def test_unverified_install_reminds_the_operator_to_keep_the_opt_out(self) -> None:
application = SimpleNamespace(
settings=SimpleNamespace(allow_unverified_downloads=True),
installer=SimpleNamespace(
install=lambda **_kwargs: (
Path("/tmp/firecracker"),
GuestAssets(kernel=Path("/tmp/vmlinux"), rootfs=Path("/tmp/rootfs")),
)
),
state_store=SimpleNamespace(initialize=lambda: None),
)
args = argparse.Namespace(command="install", force=False)
output = io.StringIO()
with redirect_stdout(output):
_run_command(application, args)
self.assertIn("UVM_ALLOW_UNVERIFIED_DOWNLOADS=1", output.getvalue())
+106
View File
@@ -0,0 +1,106 @@
from __future__ import annotations
import unittest
from ipaddress import IPv4Address, IPv4Network
from pathlib import Path
from uvm.config import Settings
from uvm.domain import VmRecord
from uvm.firecracker.api import FirecrackerClient
from uvm.firecracker.config import build_config, vcpu_count
class RecordingFirecrackerClient(FirecrackerClient):
def __init__(self) -> None:
self.calls: list[tuple[str, str, object]] = []
def _request(self, method: str, path: str, body: object) -> None:
self.calls.append((method, path, body))
class FirecrackerConfigTests(unittest.TestCase):
def test_builds_a_writable_vm_disk_configuration(self) -> None:
vm = VmRecord(
id="vm-test",
cpu=0.5,
ram_mib=512,
guest_ip="10.42.0.2",
gateway="10.42.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/firecracker.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
disk="/vm/rootfs.ext4",
)
config = build_config(
Settings(),
vm,
Path("/images/vmlinux"),
Path("/vm/rootfs.ext4"),
)
self.assertEqual(vcpu_count(0.5), 1)
self.assertEqual(config["machine-config"]["mem_size_mib"], 512)
self.assertEqual(config["drives"][0]["path_on_host"], "/vm/rootfs.ext4")
self.assertIn("ip=10.42.0.2::10.42.0.1", config["boot-source"]["boot_args"])
self.assertNotIn("password", str(config))
def test_configures_firecracker_in_required_order_before_start(self) -> None:
vm = VmRecord(
id="vm-test",
cpu=1,
ram_mib=512,
guest_ip="10.42.0.2",
gateway="10.42.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/firecracker.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
disk="/vm/rootfs.ext4",
)
config = build_config(
Settings(), vm, Path("/images/vmlinux"), Path("/vm/rootfs.ext4")
)
client = RecordingFirecrackerClient()
client.configure_and_start(config)
self.assertEqual(
[path for _method, path, _body in client.calls],
[
"/machine-config",
"/boot-source",
"/drives/rootfs",
"/network-interfaces/eth0",
"/actions",
],
)
self.assertEqual(
client.calls[0][2],
{"vcpu_count": 1, "mem_size_mib": 512, "smt": False},
)
self.assertEqual(client.calls[-1][2], {"action_type": "InstanceStart"})
def test_uses_the_configured_network_netmask_in_guest_boot_arguments(self) -> None:
settings = Settings(
network=IPv4Network("10.50.0.0/16"),
gateway=IPv4Address("10.50.0.1"),
)
vm = VmRecord(
id="vm-test",
cpu=1,
ram_mib=512,
guest_ip="10.50.0.2",
gateway="10.50.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/firecracker.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
)
config = build_config(settings, vm, Path("/images/vmlinux"), Path("/vm/rootfs.ext4"))
self.assertIn("ip=10.50.0.2::10.50.0.1:255.255.0.0", config["boot-source"]["boot_args"])
+99
View File
@@ -0,0 +1,99 @@
from __future__ import annotations
import hashlib
import tempfile
import unittest
from pathlib import Path
from uvm.config import Settings
from uvm.errors import UvmError
from uvm.images import (
_FIRECRACKER_DEMO_PUBLIC_KEY,
_enable_ssh_password_authentication,
_set_shadow_password,
_without_firecracker_demo_key,
ImageStore,
)
from uvm.integrity import write_manifest
from uvm.system import CommandResult
class PasswordHashRunner:
def __init__(self) -> None:
self.command: tuple[str, ...] | None = None
self.options: dict[str, object] = {}
def run(self, command, **options) -> CommandResult:
self.command = tuple(str(part) for part in command)
self.options = options
return CommandResult(self.command, 0, stdout="$6$salt$password-hash\n")
class ImageStoreTests(unittest.TestCase):
def test_uses_the_persisted_install_manifest_when_strict_mode_is_enabled(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
settings = Settings(base=Path(temporary_directory), allow_unverified_downloads=False)
settings.images_dir.mkdir(parents=True)
settings.kernel_image.write_bytes(b"kernel")
settings.rootfs_image.write_bytes(b"rootfs")
write_manifest(
settings.integrity_manifest_path,
{
"kernel": hashlib.sha256(b"kernel").hexdigest(),
"rootfs": hashlib.sha256(b"rootfs").hexdigest(),
"firecracker": "0" * 64,
"jailer": "1" * 64,
},
verified=True,
)
assets = ImageStore(settings).installed_assets()
self.assertEqual(assets.kernel.name, "vmlinux")
self.assertEqual(assets.rootfs.name, "ubuntu.ext4")
def test_hashes_password_through_stdin_without_putting_it_in_argv(self) -> None:
runner = PasswordHashRunner()
store = ImageStore(Settings(), runner=runner) # type: ignore[arg-type]
password_hash = store._password_hash("secret-value")
self.assertEqual(password_hash, "$6$salt$password-hash")
self.assertEqual(runner.command, ("openssl", "passwd", "-6", "-stdin"))
self.assertNotIn("secret-value", runner.command)
self.assertEqual(runner.options["input_text"], "secret-value\n")
self.assertTrue(runner.options["sensitive"])
def test_updates_only_the_requested_shadow_entry(self) -> None:
original = "root:*:1:0:99999:7:::\nservice:!:1:0:99999:7:::\n"
updated = _set_shadow_password(original, "root", "$6$salt$hash")
self.assertEqual(
updated,
"root:$6$salt$hash:1:0:99999:7:::\nservice:!:1:0:99999:7:::\n",
)
with self.assertRaises(UvmError):
_set_shadow_password(original, "missing", "$6$salt$hash")
def test_enables_root_password_login_before_existing_sshd_settings(self) -> None:
updated = _enable_ssh_password_authentication(
"PasswordAuthentication no\nPermitRootLogin prohibit-password\n",
"root",
)
self.assertTrue(
updated.startswith(
"# Managed by uvm\nPasswordAuthentication yes\nPermitRootLogin yes\n"
)
)
def test_removes_only_the_public_firecracker_demo_key(self) -> None:
own_key = "ssh-ed25519 AAAA-own-key developer@example"
updated = _without_firecracker_demo_key(
f"{_FIRECRACKER_DEMO_PUBLIC_KEY} demo\n{own_key}\n"
)
self.assertEqual(updated, f"{own_key}\n")
+70
View File
@@ -0,0 +1,70 @@
from __future__ import annotations
import hashlib
import tempfile
import unittest
from pathlib import Path
from uvm.errors import UvmError
from uvm.integrity import load_manifest, verify_file, write_manifest
class IntegrityTests(unittest.TestCase):
def test_verifies_a_matching_sha256(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
artifact = Path(temporary_directory) / "artifact"
artifact.write_bytes(b"trusted artifact")
digest = hashlib.sha256(b"trusted artifact").hexdigest()
verify_file(
artifact,
digest,
"artifact",
"UVM_ARTIFACT_SHA256",
allow_unverified=False,
)
def test_rejects_missing_or_mismatched_checksums_by_default(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
artifact = Path(temporary_directory) / "artifact"
artifact.write_bytes(b"artifact")
with self.assertRaises(UvmError):
verify_file(
artifact,
None,
"artifact",
"UVM_ARTIFACT_SHA256",
allow_unverified=False,
)
with self.assertRaises(UvmError):
verify_file(
artifact,
"0" * 64,
"artifact",
"UVM_ARTIFACT_SHA256",
allow_unverified=False,
)
def test_allows_an_explicit_local_development_opt_out(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
artifact = Path(temporary_directory) / "artifact"
artifact.write_bytes(b"artifact")
verify_file(
artifact,
None,
"artifact",
"UVM_ARTIFACT_SHA256",
allow_unverified=True,
)
def test_unverified_manifest_is_not_promoted_to_a_verified_install(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
manifest_path = Path(temporary_directory) / "integrity.json"
write_manifest(manifest_path, {"kernel": "0" * 64}, verified=False)
manifest = load_manifest(manifest_path)
self.assertFalse(manifest.verified)
self.assertEqual(manifest.checksums["kernel"], "0" * 64)
+185
View File
@@ -0,0 +1,185 @@
from __future__ import annotations
import tempfile
import unittest
from dataclasses import replace
from ipaddress import IPv4Address
from pathlib import Path
from unittest.mock import Mock, patch
from uvm.config import Settings
from uvm.domain import VmSpec
from uvm.errors import FirecrackerError, UvmError
from uvm.firecracker.process import ProcessInfo
from uvm.images import ImageStore
from uvm.lifecycle import LifecycleService
from uvm.state import StateStore
class FakeNetwork:
def __init__(self) -> None:
self.created_taps: list[str] = []
self.deleted_taps: list[str] = []
def allocate_ip(self, _vms, requested: IPv4Address | None) -> IPv4Address:
return requested or IPv4Address("10.42.0.2")
@staticmethod
def mac_for(index: int) -> str:
return f"02:fc:00:00:00:{index:02x}"
@staticmethod
def tap_name(vm_id: str) -> str:
return f"uvm-{vm_id[-8:]}"[:15]
def ensure_bridge(self) -> None:
return None
def create_tap(self, name: str) -> None:
self.created_taps.append(name)
def delete_tap(self, name: str) -> None:
self.deleted_taps.append(name)
class FakeProcess:
def __init__(self) -> None:
self.started = False
self.terminated: list[int | None] = []
self.alive = True
def start(self, _vm) -> ProcessInfo:
self.started = True
return ProcessInfo(pid=12345, start_time="42")
def is_alive(self, _vm) -> bool:
return self.alive
def terminate(self, vm) -> None:
self.terminated.append(vm.pid)
self.alive = False
class FakeClient:
def __init__(self, should_fail: bool = False) -> None:
self.should_fail = should_fail
self.config = None
def configure_and_start(self, config) -> None:
self.config = config
if self.should_fail:
raise FirecrackerError("simulated API failure")
class LifecycleTests(unittest.TestCase):
def setUp(self) -> None:
self._temporary_directory = tempfile.TemporaryDirectory()
base = Path(self._temporary_directory.name) / "uvm"
self.settings = replace(Settings(), base=base, allow_unverified_downloads=True)
self.settings.bin_dir.mkdir(parents=True)
self.settings.firecracker_binary.touch()
self.settings.images_dir.mkdir(parents=True)
self.settings.kernel_image.write_bytes(b"kernel")
self.settings.rootfs_image.write_bytes(b"rootfs")
self.store = StateStore(self.settings)
self.images = ImageStore(self.settings)
self.images.provision_credentials = Mock() # type: ignore[method-assign]
self.network = FakeNetwork()
self.process = FakeProcess()
self.client = FakeClient()
self.service = LifecycleService(
self.settings,
self.store,
self.images,
self.network, # type: ignore[arg-type]
self.process, # type: ignore[arg-type]
client_factory=lambda _socket, _timeout: self.client, # type: ignore[arg-type]
)
self._root_patch = patch("uvm.lifecycle.require_root")
self._kvm_patch = patch("uvm.lifecycle.check_kvm")
self._root_patch.start()
self._kvm_patch.start()
def tearDown(self) -> None:
self._kvm_patch.stop()
self._root_patch.stop()
self._temporary_directory.cleanup()
def test_create_uses_a_private_disk_and_persists_running_state(self) -> None:
vm = self.service.create(VmSpec(cpu=1, ram_mib=512))
self.assertEqual(vm.status, "running")
self.assertNotEqual(Path(vm.disk), self.settings.rootfs_image)
self.assertEqual(Path(vm.disk).read_bytes(), b"rootfs")
self.assertEqual(vm.username, "root")
self.assertEqual(vm.password, "root")
self.assertEqual(self.store.load().vms[vm.id].pid, 12345)
self.assertEqual(self.network.created_taps, [vm.tap])
self.assertEqual(self.client.config["drives"][0]["path_on_host"], vm.disk)
self.images.provision_credentials.assert_called_once_with(
Path(vm.disk),
"root",
"root",
)
def test_create_persists_and_provisions_custom_guest_credentials(self) -> None:
vm = self.service.create(
VmSpec(cpu=1, ram_mib=512, username="admin", password="secret-value")
)
persisted = self.store.load().vms[vm.id]
self.assertEqual(persisted.username, "admin")
self.assertEqual(persisted.password, "secret-value")
self.images.provision_credentials.assert_called_once_with(
Path(vm.disk),
"admin",
"secret-value",
)
def test_create_rolls_back_when_firecracker_configuration_fails(self) -> None:
self.client.should_fail = True
with self.assertRaises(FirecrackerError):
self.service.create(VmSpec(cpu=1, ram_mib=512))
self.assertEqual(self.store.load().vms, {})
self.assertEqual(len(self.network.deleted_taps), 1)
self.assertEqual(self.process.terminated, [12345])
self.assertEqual(list(self.settings.vms_dir.iterdir()), [])
def test_create_rolls_back_before_network_setup_when_provisioning_fails(self) -> None:
self.images.provision_credentials.side_effect = UvmError("invalid guest image")
with self.assertRaises(UvmError):
self.service.create(VmSpec(cpu=1, ram_mib=512))
self.assertEqual(self.store.load().vms, {})
self.assertEqual(self.network.created_taps, [])
self.assertFalse(self.process.started)
self.assertEqual(list(self.settings.vms_dir.iterdir()), [])
def test_create_rolls_back_when_interrupted(self) -> None:
def interrupting_create_tap(_name: str) -> None:
raise KeyboardInterrupt
self.network.create_tap = interrupting_create_tap
with self.assertRaises(KeyboardInterrupt):
self.service.create(VmSpec(cpu=1, ram_mib=512))
self.assertEqual(self.store.load().vms, {})
self.assertEqual(list(self.settings.vms_dir.iterdir()), [])
def test_stop_keeps_disk_and_destroy_releases_state(self) -> None:
vm = self.service.create(VmSpec(cpu=1, ram_mib=512))
disk = Path(vm.disk)
stopped = self.service.stop(vm.id)
self.assertEqual(stopped.status, "stopped")
self.assertTrue(disk.exists())
self.assertIsNone(stopped.pid)
destroyed = self.service.destroy(vm.id)
self.assertEqual(destroyed.id, vm.id)
self.assertFalse(self.settings.vm_dir(vm.id).exists())
self.assertEqual(self.store.load().vms, {})
+153
View File
@@ -0,0 +1,153 @@
from __future__ import annotations
import unittest
from ipaddress import IPv4Address, IPv4Network
from uvm.config import Settings
from uvm.domain import VmRecord
from uvm.errors import UvmError, ValidationError
from uvm.network import NetworkManager
from uvm.system import CommandResult
class FakeRunner:
def __init__(self) -> None:
self.calls: list[tuple[str, ...]] = []
self.existing_links: set[str] = set()
self.link_details: dict[str, str] = {}
def run(self, command, *, check=True, capture=False, timeout=None) -> CommandResult:
del check, capture, timeout
args = tuple(str(part) for part in command)
self.calls.append(args)
if args[:3] == ("ip", "link", "show") and args[3] in self.existing_links:
return CommandResult(args=args, returncode=0)
if args[:6] == ("ip", "-j", "-d", "link", "show", "dev"):
return CommandResult(args=args, returncode=0, stdout=self.link_details[args[6]])
if args[:4] == ("ip", "link", "show", "uvm0"):
return CommandResult(args=args, returncode=1)
if args == ("ip", "route", "show", "default"):
return CommandResult(args=args, returncode=0, stdout="default via 192.0.2.1 dev eth0\n")
if len(args) > 3 and args[0] == "iptables" and "-C" in args:
return CommandResult(args=args, returncode=1)
return CommandResult(args=args, returncode=0)
class NetworkManagerTests(unittest.TestCase):
def setUp(self) -> None:
self.runner = FakeRunner()
self.network = NetworkManager(Settings(), runner=self.runner) # type: ignore[arg-type]
def test_allocates_first_available_guest_address(self) -> None:
self.assertEqual(self.network.allocate_ip([], None), IPv4Address("10.42.0.2"))
def test_rejects_gateway_and_used_requested_addresses(self) -> None:
vm = VmRecord(
id="vm-test",
cpu=1,
ram_mib=512,
guest_ip="10.42.0.2",
gateway="10.42.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/firecracker.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
)
with self.assertRaises(ValidationError):
self.network.allocate_ip([vm], IPv4Address("10.42.0.1"))
with self.assertRaises(ValidationError):
self.network.allocate_ip([vm], IPv4Address("10.42.0.2"))
with self.assertRaises(ValidationError):
self.network.allocate_ip([], IPv4Address("10.42.0.0"))
with self.assertRaises(ValidationError):
self.network.allocate_ip([], IPv4Address("10.42.0.255"))
def test_mac_and_tap_names_are_bounded_and_deterministic(self) -> None:
self.assertEqual(NetworkManager.mac_for(1), "02:fc:00:00:00:01")
tap = NetworkManager.tap_name("vm-0123456789abcdef")
self.assertLessEqual(len(tap), 15)
self.assertTrue(tap.startswith("uvm-"))
def test_bridge_setup_adds_a_missing_bridge_and_one_nat_rule(self) -> None:
self.network.ensure_bridge()
self.assertIn(("ip", "link", "add", "uvm0", "type", "bridge"), self.runner.calls)
self.assertIn(
("ip", "addr", "replace", "10.42.0.1/24", "dev", "uvm0"),
self.runner.calls,
)
self.assertIn(
(
"iptables",
"-A",
"FORWARD",
"-i",
"uvm0",
"-o",
"eth0",
"-s",
"10.42.0.0/24",
"-j",
"ACCEPT",
),
self.runner.calls,
)
checks = [
call
for call in self.runner.calls
if call[:5] == ("iptables", "-t", "nat", "-C", "POSTROUTING")
]
self.assertEqual(len(checks), 1)
self.assertIn(
(
"iptables",
"-t",
"nat",
"-A",
"POSTROUTING",
"-s",
"10.42.0.0/24",
"-o",
"eth0",
"-j",
"MASQUERADE",
),
self.runner.calls,
)
def test_bridge_and_guest_network_support_non_default_prefixes(self) -> None:
settings = Settings(
network=IPv4Network("10.50.0.0/16"),
gateway=IPv4Address("10.50.0.1"),
)
runner = FakeRunner()
NetworkManager(settings, runner=runner).ensure_bridge() # type: ignore[arg-type]
self.assertIn(
("ip", "addr", "replace", "10.50.0.1/16", "dev", "uvm0"),
runner.calls,
)
def test_refuses_to_adopt_an_existing_tap_device(self) -> None:
self.runner.existing_links.add("uvm-existing")
with self.assertRaises(UvmError):
self.network.create_tap("uvm-existing")
self.assertNotIn(
("ip", "tuntap", "add", "dev", "uvm-existing", "mode", "tap"),
self.runner.calls,
)
def test_refuses_to_reconfigure_an_existing_non_bridge_interface(self) -> None:
self.runner.existing_links.add("uvm0")
self.runner.link_details["uvm0"] = '[{"linkinfo": {"info_kind": "dummy"}}]'
with self.assertRaises(UvmError):
self.network.ensure_bridge()
self.assertNotIn(
("ip", "addr", "replace", "10.42.0.1/24", "dev", "uvm0"),
self.runner.calls,
)
+78
View File
@@ -0,0 +1,78 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from uvm.config import Settings
from uvm.domain import VmRecord
from uvm.errors import FirecrackerError
from uvm.firecracker.process import FirecrackerProcessManager
def make_vm(pid: int | None, start_time: str | None) -> VmRecord:
return VmRecord(
id="vm-test",
cpu=1,
ram_mib=512,
guest_ip="10.42.0.2",
gateway="10.42.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/uvm-missing.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
pid=pid,
process_start_time=start_time,
)
class FirecrackerProcessSafetyTests(unittest.TestCase):
def test_refuses_to_signal_an_unidentified_legacy_pid(self) -> None:
manager = FirecrackerProcessManager(Settings())
vm = make_vm(pid=12345, start_time=None)
with patch("uvm.firecracker.process.os.kill") as kill:
with self.assertRaises(FirecrackerError):
manager.terminate(vm)
kill.assert_called_once_with(12345, 0)
def test_allows_cleanup_when_an_unidentified_legacy_pid_is_already_gone(self) -> None:
manager = FirecrackerProcessManager(Settings())
vm = make_vm(pid=12345, start_time=None)
with patch("uvm.firecracker.process.os.kill", side_effect=ProcessLookupError):
manager.terminate(vm)
def test_rejects_non_positive_pids_without_signaling(self) -> None:
manager = FirecrackerProcessManager(Settings())
vm = make_vm(pid=0, start_time="42")
with patch("uvm.firecracker.process.os.kill") as kill:
with self.assertRaises(FirecrackerError):
manager.terminate(vm)
kill.assert_not_called()
def test_interrupt_during_startup_terminates_the_child_process(self) -> None:
class FakePopen:
pid = 12345
def poll(self) -> int | None:
return None
with tempfile.TemporaryDirectory() as temporary_directory:
settings = Settings(base=Path(temporary_directory), terminate_timeout_s=0)
manager = FirecrackerProcessManager(settings, popen=lambda *_args, **_kwargs: FakePopen())
manager._wait_for_socket = lambda *_args: (_ for _ in ()).throw(KeyboardInterrupt)
vm = make_vm(pid=None, start_time=None)
vm.log = str(Path(temporary_directory) / "firecracker.log")
vm.socket = str(Path(temporary_directory) / "firecracker.sock")
with patch("uvm.firecracker.process.os.kill") as kill:
with self.assertRaises(KeyboardInterrupt):
manager.start(vm)
self.assertEqual(kill.call_count, 2)
+241
View File
@@ -0,0 +1,241 @@
from __future__ import annotations
import argparse
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from uvm.cli import build_parser, main
from uvm.config import Settings
from uvm.errors import ConfigurationError, UvmError
from uvm.server import create_api, run_server
def fake_application(*, api_token: str | None = None):
settings = SimpleNamespace(
app_name="uvm",
default_vcpu=1,
default_ram_mib=512,
default_ssh_user="root",
api_token=api_token,
api_tls_cert=None,
api_tls_key=None,
)
return SimpleNamespace(settings=settings)
class ServerCliTests(unittest.TestCase):
def test_parser_accepts_the_requested_serve_invocation(self) -> None:
parser = build_parser(fake_application())
args = parser.parse_args(["--serve", "--port", "8123", "--host", "127.0.0.1"])
self.assertTrue(args.serve)
self.assertEqual(args.port, 8123)
self.assertEqual(args.host, "127.0.0.1")
self.assertIsNone(args.command)
def test_parser_rejects_an_invalid_server_port(self) -> None:
parser = build_parser(fake_application())
with self.assertRaises(SystemExit):
parser.parse_args(["--serve", "--port", "70000"])
def test_create_parser_defaults_guest_credentials_to_root(self) -> None:
args = build_parser(fake_application()).parse_args(["create"])
self.assertEqual(args.username, "root")
self.assertEqual(args.password, "root")
def test_main_delegates_serve_mode_to_the_server_launcher(self) -> None:
application = fake_application()
with (
patch("uvm.cli.build_application", return_value=application),
patch("uvm.server.run_server") as run_server_mock,
):
result = main(["--serve", "--host", "127.0.0.1", "--port", "8123"])
self.assertEqual(result, 0)
run_server_mock.assert_called_once_with(application, host="127.0.0.1", port=8123)
def test_server_requires_an_api_token_even_on_loopback(self) -> None:
with self.assertRaises(UvmError):
run_server(fake_application(api_token=None), host="127.0.0.1", port=8000)
def test_non_loopback_server_requires_tls(self) -> None:
with self.assertRaises(UvmError):
run_server(fake_application(api_token="test-token"), host="0.0.0.0", port=8000)
def test_factory_requires_an_explicit_host(self) -> None:
with self.assertRaises(UvmError):
create_api(fake_application(api_token="test-token"))
def test_invalid_tls_material_is_rejected_before_uvicorn_starts(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
certificate = Path(temporary_directory) / "cert.pem"
key = Path(temporary_directory) / "key.pem"
certificate.touch()
key.touch()
application = fake_application(api_token="test-token")
application.settings.api_tls_cert = certificate
application.settings.api_tls_key = key
with self.assertRaises(UvmError):
run_server(application, host="127.0.0.1", port=8000)
def test_non_loopback_server_uses_configured_tls(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
certificate = Path(temporary_directory) / "cert.pem"
key = Path(temporary_directory) / "key.pem"
certificate.touch()
key.touch()
application = fake_application(api_token="test-token")
application.settings.api_tls_cert = certificate
application.settings.api_tls_key = key
fake_uvicorn = SimpleNamespace(run=lambda *_args, **_kwargs: None)
with (
patch("uvm.server._validate_server_settings"),
patch("uvm.server.create_api", return_value=object()),
patch.dict(sys.modules, {"uvicorn": fake_uvicorn}),
patch.object(fake_uvicorn, "run") as run_mock,
):
run_server(application, host="0.0.0.0", port=8443)
run_mock.assert_called_once()
self.assertEqual(run_mock.call_args.kwargs["ssl_certfile"], str(certificate))
self.assertEqual(run_mock.call_args.kwargs["ssl_keyfile"], str(key))
def test_api_token_rejects_non_ascii_or_whitespace(self) -> None:
for token in ("s\u00e9cret", "contains space", ""):
with self.subTest(token=token):
with self.assertRaises(ConfigurationError):
Settings(api_token=token)
FASTAPI_AVAILABLE = (
importlib.util.find_spec("fastapi") is not None
and importlib.util.find_spec("httpx") is not None
)
if FASTAPI_AVAILABLE:
from fastapi.testclient import TestClient
from uvm.domain import VmRecord
from uvm.lifecycle import ListedVm
from uvm.server import create_api
class FakeLifecycle:
def __init__(self) -> None:
self.last_spec = None
self.vm = VmRecord(
id="vm-test",
cpu=1,
ram_mib=512,
guest_ip="10.42.0.2",
gateway="10.42.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/firecracker.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
status="running",
)
def list_vms(self) -> list[ListedVm]:
return [ListedVm(vm=self.vm, observed_status=self.vm.status)]
def create(self, spec):
self.last_spec = spec
self.vm.username = spec.username
self.vm.password = spec.password
self.vm.status = "running"
return self.vm
def stop(self, _vm_id: str):
self.vm.status = "stopped"
return self.vm
def destroy(self, _vm_id: str):
return self.vm
@unittest.skipUnless(FASTAPI_AVAILABLE, "FastAPI is not installed")
class ApiRouterTests(unittest.TestCase):
def setUp(self) -> None:
application = fake_application(api_token="test-token")
self.lifecycle = FakeLifecycle()
application.lifecycle = self.lifecycle
application.installer = SimpleNamespace(
install=lambda **_kwargs: (
Path("/tmp/firecracker"),
SimpleNamespace(kernel=Path("/tmp/vmlinux"), rootfs=Path("/tmp/rootfs")),
)
)
application.state_store = SimpleNamespace(initialize=lambda: None)
self.client = TestClient(create_api(application, host="127.0.0.1"))
def test_health_is_available_without_credentials(self) -> None:
response = self.client.get("/health")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok"})
def test_vm_routes_require_and_accept_the_api_token(self) -> None:
unauthorized = self.client.get("/vms")
authorized = self.client.get("/vms", headers={"X-UVM-Token": "test-token"})
self.assertEqual(unauthorized.status_code, 401)
self.assertEqual(authorized.status_code, 200)
self.assertEqual(authorized.json()[0]["id"], "vm-test")
self.assertNotIn("password", authorized.json()[0])
def test_vm_lifecycle_routes_and_extra_field_validation(self) -> None:
headers = {"X-UVM-Token": "test-token"}
created = self.client.post(
"/vms",
headers=headers,
json={
"cpu": 1,
"ram": "512",
"username": "root",
"password": "api-secret",
},
)
detail = self.client.get("/vms/vm-test", headers=headers)
stopped = self.client.post("/vms/vm-test/stop", headers=headers)
destroyed = self.client.delete("/vms/vm-test", headers=headers)
invalid = self.client.post("/vms", headers=headers, json={"forse": True})
self.assertEqual(created.status_code, 201)
self.assertEqual(created.json()["username"], "root")
self.assertNotIn("password", created.json())
self.assertEqual(self.lifecycle.last_spec.password, "api-secret")
self.assertEqual(detail.status_code, 200)
self.assertEqual(stopped.json()["status"], "stopped")
self.assertEqual(destroyed.json()["status"], "terminated")
self.assertEqual(invalid.status_code, 422)
def test_vm_create_defaults_guest_credentials_to_root(self) -> None:
response = self.client.post(
"/vms",
headers={"X-UVM-Token": "test-token"},
json={"cpu": 1, "ram": "512"},
)
self.assertEqual(response.status_code, 201)
self.assertEqual(self.lifecycle.last_spec.username, "root")
self.assertEqual(self.lifecycle.last_spec.password, "root")
def test_install_route_uses_the_existing_installer(self) -> None:
response = self.client.post(
"/install",
headers={"X-UVM-Token": "test-token"},
json={"force": True},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["firecracker"], "/tmp/firecracker")
+90
View File
@@ -0,0 +1,90 @@
from __future__ import annotations
import json
import stat
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
from uvm.config import Settings
from uvm.domain import VmRecord
from uvm.state import StateStore
def make_vm(vm_id: str = "vm-test") -> VmRecord:
return VmRecord(
id=vm_id,
cpu=1,
ram_mib=512,
guest_ip="10.42.0.2",
gateway="10.42.0.1",
tap="uvm-test",
mac="02:fc:00:00:00:01",
socket="/tmp/firecracker.sock",
config="/tmp/config.json",
log="/tmp/firecracker.log",
disk="/tmp/rootfs.ext4",
username="admin",
password="stored-secret",
)
class StateStoreTests(unittest.TestCase):
def setUp(self) -> None:
self._temporary_directory = tempfile.TemporaryDirectory()
self.base = Path(self._temporary_directory.name) / "uvm"
self.settings = replace(Settings(), base=self.base)
self.store = StateStore(self.settings)
def tearDown(self) -> None:
self._temporary_directory.cleanup()
def test_transaction_persists_a_vm_atomically(self) -> None:
self.store.initialize()
with self.store.transaction() as state:
state.vms["vm-test"] = make_vm()
state.next_mac_index = 2
loaded = self.store.load()
self.assertEqual(loaded.vms["vm-test"].disk, "/tmp/rootfs.ext4")
self.assertEqual(loaded.vms["vm-test"].username, "admin")
self.assertEqual(loaded.vms["vm-test"].password, "stored-secret")
self.assertNotIn("stored-secret", repr(loaded.vms["vm-test"]))
self.assertEqual(loaded.next_mac_index, 2)
self.assertEqual(stat.S_IMODE(self.settings.state_path.stat().st_mode), 0o600)
persisted = json.loads(self.settings.state_path.read_text(encoding="utf-8"))
self.assertEqual(persisted["vms"]["vm-test"]["username"], "admin")
self.assertEqual(persisted["vms"]["vm-test"]["password"], "stored-secret")
def test_legacy_state_is_loaded_and_migrated_on_next_write(self) -> None:
self.base.mkdir(parents=True)
legacy = {"vms": {"vm-test": make_vm().to_dict()}}
legacy["vms"]["vm-test"].pop("disk")
legacy["vms"]["vm-test"].pop("username")
legacy["vms"]["vm-test"].pop("password")
legacy["vms"]["vm-test"].pop("updated_at")
self.settings.state_path.write_text(json.dumps(legacy), encoding="utf-8")
loaded = self.store.load()
self.assertEqual(loaded.vms["vm-test"].disk, "")
self.assertEqual(loaded.vms["vm-test"].username, "root")
self.assertIsNone(loaded.vms["vm-test"].password)
self.assertEqual(loaded.next_mac_index, 2)
with self.store.transaction() as state:
state.vms["vm-test"].status = "stopped"
persisted = json.loads(self.settings.state_path.read_text(encoding="utf-8"))
self.assertEqual(persisted["schema_version"], 1)
self.assertEqual(persisted["next_mac_index"], 2)
self.assertEqual(persisted["vms"]["vm-test"]["status"], "stopped")
def test_initialize_protects_registry_credentials(self) -> None:
self.base.mkdir(parents=True)
self.settings.state_path.write_text('{"vms": {}}', encoding="utf-8")
self.settings.state_path.chmod(0o644)
self.store.initialize()
self.assertEqual(stat.S_IMODE(self.settings.state_path.stat().st_mode), 0o600)
+46
View File
@@ -0,0 +1,46 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from uvm.errors import CommandError
from uvm.system import CommandRunner
class CommandRunnerTests(unittest.TestCase):
def test_passes_sensitive_input_over_stdin_without_logging_it(self) -> None:
emitted: list[str] = []
completed = SimpleNamespace(returncode=0, stdout="result\n", stderr="")
with patch("uvm.system.subprocess.run", return_value=completed) as run:
result = CommandRunner(emit=emitted.append).run(
("credential-tool", "--stdin"),
capture=True,
input_text="secret-value\n",
sensitive=True,
)
self.assertEqual(result.stdout, "result\n")
self.assertEqual(run.call_args.kwargs["input"], "secret-value\n")
self.assertNotIn("secret-value", emitted[0])
def test_redacts_sensitive_command_output_from_errors(self) -> None:
completed = SimpleNamespace(
returncode=1,
stdout="",
stderr="failure mentioning secret-value",
)
with (
patch("uvm.system.subprocess.run", return_value=completed),
self.assertRaises(CommandError) as raised,
):
CommandRunner(emit=None).run(
("credential-tool", "--stdin"),
capture=True,
input_text="secret-value\n",
sensitive=True,
)
self.assertNotIn("secret-value", str(raised.exception))
+45
View File
@@ -0,0 +1,45 @@
from __future__ import annotations
import unittest
from uvm.errors import ValidationError
from uvm.validation import parse_cpu, parse_password, parse_ram, parse_username
class ValidationTests(unittest.TestCase):
def test_parse_ram_uses_mib_by_default(self) -> None:
self.assertEqual(parse_ram("512"), 512)
self.assertEqual(parse_ram("1G"), 1024)
self.assertEqual(parse_ram("512MiB"), 512)
self.assertEqual(parse_ram("134217728B"), 128)
def test_parse_ram_rejects_small_and_invalid_values(self) -> None:
for value in ("127M", "nonsense", "-1G", "1T"):
with self.subTest(value=value):
with self.assertRaises(ValidationError):
parse_ram(value)
def test_parse_cpu_requires_a_positive_finite_value(self) -> None:
self.assertEqual(parse_cpu("0.5"), 0.5)
for value in ("0", "-1", "nan", "inf", "cpu"):
with self.subTest(value=value):
with self.assertRaises(ValidationError):
parse_cpu(value)
def test_guest_username_is_validated(self) -> None:
self.assertEqual(parse_username("root"), "root")
self.assertEqual(parse_username("app-user"), "app-user")
for value in ("", "Root", "9user", "user name", "a" * 33):
with self.subTest(value=value):
with self.assertRaises(ValidationError):
parse_username(value)
def test_guest_password_rejects_empty_control_or_oversized_values(self) -> None:
self.assertEqual(
parse_password("correct horse battery staple"),
"correct horse battery staple",
)
for value in ("", "line\nbreak", "tab\tvalue", "a" * 129):
with self.subTest(value=value):
with self.assertRaises(ValidationError):
parse_password(value)