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 Mock, patch from uvm.cli import build_parser, main from uvm.config import Settings from uvm.domain import PortRule, VmRecord, VmResources, VmSpec from uvm.errors import ( ConfigurationError, ConflictError, GuestAgentUnavailableError, GuestExecutionError, NotFoundError, UvmError, ValidationError, ) from uvm.guest_exec import ExecResult from uvm.lifecycle import ListedVm, OperationResult from uvm.server import _http_status_for, 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_storage_mib=30 * 1024, 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 api = SimpleNamespace(state=SimpleNamespace()) server = SimpleNamespace(run=Mock()) config = object() fake_uvicorn = SimpleNamespace( Config=Mock(return_value=config), Server=Mock(return_value=server), ) with ( patch("uvm.server._validate_server_settings"), patch("uvm.server.create_api", return_value=api), patch.dict(sys.modules, {"uvicorn": fake_uvicorn}), ): run_server(application, host="0.0.0.0", port=8443) fake_uvicorn.Config.assert_called_once_with( api, host="0.0.0.0", port=8443, ssl_certfile=str(certificate), ssl_keyfile=str(key), ) fake_uvicorn.Server.assert_called_once_with(config) self.assertIs(api.state.uvicorn_server, server) server.run.assert_called_once_with() 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) class ServerErrorTests(unittest.TestCase): def test_domain_errors_map_to_http_status_codes(self) -> None: for error, expected in ( (NotFoundError("unknown guest"), 404), (ConflictError("ambiguous name"), 409), (ValidationError("invalid name"), 422), (ConfigurationError("invalid settings"), 422), (UvmError("this operation needs root"), 403), (UvmError("host operation failed"), 500), (GuestAgentUnavailableError("not ready"), 503), (GuestExecutionError("lost connection", outcome_unknown=True), 502), ): with self.subTest(error=error): self.assertEqual(_http_status_for(error), expected) 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 class FakeLifecycle: def __init__(self) -> None: self.last_spec: VmSpec | None = None self.last_resize: dict | None = None self.last_execution: dict | None = None self.observed_status = "running" self.policy_status = "enforced" self.vm = VmRecord( id="vm-test", name="test-vm", cpu=1, ram_mib=512, storage_mib=30 * 1024, 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", password="guest-secret", status="running", ports=(PortRule(22),), operation=None, disk_ready=True, boot_id="test-boot", ) def list_vms(self) -> list[ListedVm]: return [ListedVm( vm=self.vm, observed_status=self.observed_status, policy_status=self.policy_status, )] def inspect(self, identifier: str) -> ListedVm: if identifier not in {self.vm.id, self.vm.guest_ip, self.vm.name}: raise NotFoundError(f"VM not found: {identifier}") return ListedVm( vm=self.vm, observed_status=self.observed_status, policy_status=self.policy_status, ) def create(self, spec: VmSpec) -> VmRecord: self.last_spec = spec self.vm.cpu = spec.cpu self.vm.ram_mib = spec.ram_mib self.vm.storage_mib = spec.storage_mib if spec.guest_ip is not None: self.vm.guest_ip = str(spec.guest_ip) self.vm.username = spec.username self.vm.password = spec.password self.vm.name = spec.name self.vm.ports = spec.ports self.vm.status = "running" self.vm.operation = None self.observed_status = "running" self.policy_status = "enforced" return self.vm def start(self, identifier: str) -> VmRecord: self.inspect(identifier) self.vm.status = "running" self.observed_status = "running" self.policy_status = "enforced" return self.vm def restart(self, identifier: str, *, timeout: float | None = None) -> VmRecord: self.inspect(identifier) self.vm.status = "running" self.observed_status = "running" self.policy_status = "enforced" return self.vm def stop(self, identifier: str, *, timeout: float | None = None) -> VmRecord: self.inspect(identifier) self.vm.status = "stopped" self.vm.pid = None self.vm.operation = None self.observed_status = "stopped" self.policy_status = "inactive" return self.vm def resize(self, identifier: str, *, cpu=None, ram_mib=None, storage_mib=None, timeout=None) -> VmRecord: self.inspect(identifier) self.last_resize = dict(cpu=cpu, ram_mib=ram_mib, storage_mib=storage_mib, timeout=timeout) for key, value in (("cpu", cpu), ("ram_mib", ram_mib), ("storage_mib", storage_mib)): if value is not None: setattr(self.vm, key, value) self.vm.pending_resize = None self.vm.operation = None return self.start(identifier) def destroy(self, identifier: str, *, timeout: float | None = None) -> VmRecord: self.inspect(identifier) return self.vm def read_log_tail(self, identifier: str, *, tail: int) -> SimpleNamespace: self.inspect(identifier) return SimpleNamespace(content=f"last {tail} log lines", truncated=False) def execute(self, vm_id: str, command: str, *, sudo: bool, timeout_seconds: float) -> ExecResult: if vm_id != self.vm.id: raise NotFoundError(f"VM not found: {vm_id}") self.last_execution = dict(vm_id=vm_id, command=command, sudo=sudo, timeout_seconds=timeout_seconds) return ExecResult(7, "hello\n", "problem\n", False, False) def install_guest_agent(self, vm_id: str) -> VmRecord: if vm_id != self.vm.id: raise NotFoundError(f"VM not found: {vm_id}") if self.vm.status != "stopped": raise ConflictError("stop the VM before installing the guest agent") return self.vm def diagnostics(self) -> list[dict]: return [{"name": "kvm", "ok": True, "message": "available"}] def resolve_targets(self, identifiers: list[str], *, all_vms: bool = False) -> list[VmRecord]: if all_vms and identifiers: raise ValidationError("--all cannot be combined with explicit VM targets") if not all_vms and not identifiers: raise ValidationError("specify a VM or --all") if all_vms: return [self.vm] return [self.inspect(identifier).vm for identifier in identifiers] def run_many( self, action: str, vm_ids: list[str], *, timeout: float | None = None, ) -> list[OperationResult]: return [OperationResult(vm_id, action, "succeeded") for vm_id in vm_ids] def set_ports(self, identifier: str, ports: tuple[PortRule, ...], *, mode: str = "set") -> VmRecord: self.inspect(identifier) if mode == "set": self.vm.ports = ports elif mode == "open": existing = {p.port: p for p in self.vm.ports or ()} for p in ports: existing.setdefault(p.port, p) self.vm.ports = tuple(sorted(existing.values(), key=lambda r: r.port)) elif mode == "close": drop = {p.port for p in ports} self.vm.ports = tuple(p for p in self.vm.ports or () if p.port not in drop) self.policy_status = "enforced" return self.vm def sync_ports(self, identifier: str) -> VmRecord: self.inspect(identifier) self.policy_status = "enforced" return self.vm @unittest.skipUnless(FASTAPI_AVAILABLE, "FastAPI and httpx are required for API tests") 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.headers = {"X-UVM-Token": "test-token"} self.api = create_api(application, host="127.0.0.1") self.client = TestClient(self.api) self.addCleanup(self.client.close) 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_standalone_uvm_api_does_not_serve_the_nest_dashboard(self) -> None: for path in ("/", "/dashboard", "/dashboard/static/js/dashboard.js"): with self.subTest(path=path): self.assertEqual(self.client.get(path).status_code, 404) def test_vm_routes_require_and_accept_the_api_token(self) -> None: for method, path, payload in ( ("GET", "/vms", {}), ("POST", "/vms", {}), ("GET", "/vms/test-vm", {}), ("GET", "/vms/test-vm/logs", {}), ("POST", "/vms/actions", {"action": "start", "identifiers": ["vm-test"]}), ("POST", "/vms/test-vm/start", {}), ("POST", "/vms/test-vm/restart", {}), ("POST", "/vms/test-vm/resize", {"cpu": 2}), ("POST", "/vms/test-vm/stop", {}), ("DELETE", "/vms/test-vm", {}), ("POST", "/vms/test-vm/ports", {}), ("POST", "/vms/test-vm/ports/sync", {}), ("POST", "/vms/vm-test/exec", {"command": "id"}), ("POST", "/vms/vm-test/agent/install", {}), ("GET", "/diagnostics", {}), ("POST", "/exit", {}), ("POST", "/install", {}), ): for headers in ({}, {"X-UVM-Token": "wrong-token"}): with self.subTest(method=method, path=path, headers=headers): response = self.client.request(method, path, headers=headers, json=payload) self.assertEqual(response.status_code, 401) self.assertIsNone(self.lifecycle.last_spec) self.assertEqual(self.lifecycle.vm.status, "running") authorized = self.client.get("/vms", headers=self.headers) self.assertEqual(authorized.status_code, 200) self.assertEqual(authorized.json()[0]["id"], "vm-test") self.assertNotIn("password", authorized.json()[0]) def test_exit_requests_a_graceful_authenticated_server_shutdown(self) -> None: server = SimpleNamespace(should_exit=False) self.api.state.uvicorn_server = server response = self.client.post("/exit", headers=self.headers) self.assertEqual(response.status_code, 202) self.assertEqual(response.json(), {"status": "stopping"}) self.assertTrue(server.should_exit) def test_exit_requires_a_running_uvicorn_server(self) -> None: response = self.client.post("/exit", headers=self.headers) self.assertEqual(response.status_code, 503) self.assertEqual(response.json(), { "detail": "server shutdown is unavailable outside nest --serve or uvm --serve", }) def test_vm_lifecycle_routes_and_extra_field_validation(self) -> None: created = self.client.post( "/vms", headers=self.headers, json={ "cpu": 1, "ram": "512", "username": "root", "password": "api-secret", }, ) detail = self.client.get("/vms/vm-test", headers=self.headers) stopped = self.client.post("/vms/vm-test/stop", headers=self.headers) destroyed = self.client.delete("/vms/vm-test", headers=self.headers) invalid = self.client.post("/vms", headers=self.headers, json={"forse": True}) self.assertEqual(created.status_code, 201) self.assertEqual(created.json()["username"], "root") self.assertEqual(self.lifecycle.last_spec.password, "api-secret") for response in (created, detail, stopped, destroyed): self.assertNotIn("password", response.json()) self.assertNotIn("api-secret", response.text) self.assertEqual(detail.status_code, 200) self.assertEqual(stopped.json()["status"], "stopped") self.assertEqual(stopped.json()["policy_status"], "inactive") self.assertEqual(destroyed.json(), {"id": "vm-test", "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=self.headers, 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") self.assertIsNone(self.lifecycle.last_spec.name) self.assertEqual(self.lifecycle.last_spec.ports, (PortRule(22),)) self.assertEqual(self.lifecycle.last_spec.storage_mib, 30 * 1024) self.assertEqual(response.json()["storage_mib"], 30 * 1024) def test_create_accepts_custom_storage_and_rejects_capacities_below_minimum(self) -> None: for capacity, expected in ((50, 50 * 1024), ("50G", 50 * 1024), ("1T", 1024 * 1024)): with self.subTest(capacity=capacity): response = self.client.post("/vms", headers=self.headers, json={"storage": capacity}) self.assertEqual(response.status_code, 201, response.text) self.assertEqual(self.lifecycle.last_spec.storage_mib, expected) self.assertEqual(response.json()["storage_mib"], expected) self.lifecycle.last_spec = None for capacity in ("29G", 29, "30M", "bad", True, None, [], {}): with self.subTest(capacity=capacity): response = self.client.post("/vms", headers=self.headers, json={"storage": capacity}) self.assertEqual(response.status_code, 422, response.text) self.assertIsNone(self.lifecycle.last_spec) def test_resize_accepts_partial_resources_and_returns_the_same_vm_identity(self) -> None: before = self.lifecycle.vm.to_dict() response = self.client.post("/vms/test-vm/resize?timeout=0.5", headers=self.headers, json={"cpu": 2, "ram": "4G", "storage": "50G"}) self.assertEqual(response.status_code, 200, response.text) self.assertEqual(self.lifecycle.last_resize, {"cpu": 2, "ram_mib": 4096, "storage_mib": 51200, "timeout": 0.5}) for field in ("id", "name", "guest_ip", "gateway", "mac", "username"): self.assertEqual(response.json()[field], before[field]) self.assertEqual(response.json()["status"], "running") self.assertEqual(response.json()["policy_status"], "enforced") self.assertEqual(response.json()["storage_mib"], 51200) self.assertIsNone(response.json()["pending_resize"]) self.assertNotIn("password", response.json()) self.assertEqual(self.lifecycle.vm.password, before["password"]) partial = self.client.post("/vms/10.42.0.2/resize", headers=self.headers, json={"ram": "2G"}) self.assertEqual(partial.status_code, 200) self.assertEqual(self.lifecycle.last_resize, {"cpu": None, "ram_mib": 2048, "storage_mib": None, "timeout": None}) self.assertEqual(partial.json()["storage_mib"], 51200) def test_resize_rejects_invalid_or_non_resource_edits_before_lifecycle_calls(self) -> None: for payload in ({}, {"cpu": None}, {"cpu": 0}, {"cpu": True}, {"ram": "127M"}, {"storage": "29G"}, {"storage": False}, {"storage": []}, {"cpu": 2, "username": "new-user"}, {"cpu": 2, "password": "new-secret"}, {"cpu": 2, "guest_ip": "10.42.0.9"}): with self.subTest(payload=payload): response = self.client.post("/vms/vm-test/resize", headers=self.headers, json=payload) self.assertEqual(response.status_code, 422, response.text) self.assertIsNone(self.lifecycle.last_resize) for timeout in ("-1", "nan", "inf"): response = self.client.post(f"/vms/vm-test/resize?timeout={timeout}", headers=self.headers, json={"cpu": 2}) self.assertEqual(response.status_code, 422) self.assertIsNone(self.lifecycle.last_resize) def test_resize_errors_and_pending_resources_are_exposed_without_credentials(self) -> None: for error, expected in ((NotFoundError("missing"), 404), (ConflictError("unknown process"), 409), (UvmError("filesystem does not fit"), 500)): with patch.object(self.lifecycle, "resize", side_effect=error): response = self.client.post("/vms/vm-test/resize", headers=self.headers, json={"storage": "30G"}) self.assertEqual(response.status_code, expected) self.assertEqual(response.json(), {"error": {"message": str(error)}}) target = VmResources(2, 4096, 51200) self.lifecycle.vm.pending_resize = target response = self.client.get("/vms/vm-test", headers=self.headers) self.assertEqual(response.json()["pending_resize"], target.to_dict()) self.assertNotIn("guest-secret", response.text) def test_create_accepts_valid_names(self) -> None: for name in (None, "a", "web-1", "a" * 63): with self.subTest(name=name): response = self.client.post("/vms", headers=self.headers, json={"name": name}) self.assertEqual(response.status_code, 201) self.assertEqual(self.lifecycle.last_spec.name, name) self.assertEqual(response.json()["name"], name) def test_create_rejects_invalid_names_before_lifecycle_calls(self) -> None: for name in ("", "-web", "9web", "Web", "web_name", "web.name", "two words", "a" * 64, 1, True): with self.subTest(name=name): response = self.client.post("/vms", headers=self.headers, json={"name": name}) self.assertEqual(response.status_code, 422) self.assertIsNone(self.lifecycle.last_spec) def test_create_normalizes_private_ports_and_default_ssh(self) -> None: cases = ( ({}, (PortRule(22),)), ({"ports": None}, (PortRule(22),)), ({"ports": []}, (PortRule(22),)), ( {"ports": [{"port": 443, "protocol": "HTTPS"}, {"port": 80, "protocol": "HTTP"}]}, (PortRule(22), PortRule(80, "HTTP"), PortRule(443, "HTTPS")), ), ( {"ports": [{"port": 80}, {"port": 22}, {"port": 80, "protocol": "TCP"}]}, (PortRule(22), PortRule(80)), ), ({"ports": [{"port": 22, "protocol": "HTTP"}]}, (PortRule(22, "HTTP"),)), ({"ports": [{"port": 65535}, {"port": 1}]}, (PortRule(1), PortRule(22), PortRule(65535))), ({"no_default_ports": True}, ()), ({"no_default_ports": True, "ports": None}, ()), ({"no_default_ports": True, "ports": []}, ()), ( {"no_default_ports": True, "ports": [{"port": 443, "protocol": "HTTPS"}]}, (PortRule(443, "HTTPS"),), ), ({"no_default_ports": True, "ports": [{"port": 22}]}, (PortRule(22),)), ) for payload, expected in cases: with self.subTest(payload=payload): response = self.client.post("/vms", headers=self.headers, json=payload) self.assertEqual(response.status_code, 201, response.text) self.assertEqual(self.lifecycle.last_spec.ports, expected) self.assertEqual(response.json()["ports"], [rule.to_dict() for rule in expected]) self.assertEqual(response.json()["policy_status"], "enforced") def test_create_rejects_invalid_ports_before_lifecycle_calls(self) -> None: invalid_rules = [ {"port": value} for value in (0, -1, 65536, True, False, 22.0, 22.5, "22", None, {}, []) ] + [ {"port": 80, "protocol": value} for value in ("UDP", "http", "HTTP ", "", None, True, 1) ] + [ {}, {"protocol": "TCP"}, {"port": 80, "public_port": 8080}, {"port": 80, "host_port": 8080}, ] payloads = [{"ports": [rule]} for rule in invalid_rules] + [ {"ports": value} for value in ("22:TCP", {}, [22], ["22:TCP"], [None]) ] + [ {"ports": [{"port": 80}, {"port": 80, "protocol": "HTTP"}]}, {"ports": [{"port": 443, "protocol": "HTTP"}, {"port": 443, "protocol": "HTTPS"}]}, {"public_ports": [80]}, ] + [ {"no_default_ports": value} for value in (None, 0, 1, "true", "false") ] for payload in payloads: with self.subTest(payload=payload): response = self.client.post("/vms", headers=self.headers, json=payload) self.assertEqual(response.status_code, 422, response.text) self.assertIsNone(self.lifecycle.last_spec) def test_list_and_get_whitelist_fields_and_preserve_observed_policy(self) -> None: expected_fields = { "id", "name", "cpu", "ram_mib", "storage_mib", "pending_resize", "guest_ip", "gateway", "mac", "username", "status", "observed_status", "ports", "policy_status", "operation", "pid", "created_at", "updated_at", "last_error", } self.lifecycle.vm.operation = "ports" self.lifecycle.observed_status = "dead" for ports, policy_status in ( (None, "unmanaged"), ((), "enforced"), ((PortRule(443, "HTTPS"),), "missing"), ((PortRule(22),), "unknown"), ((PortRule(22),), "drift"), ): with self.subTest(ports=ports, policy_status=policy_status): self.lifecycle.vm.ports = ports self.lifecycle.policy_status = policy_status listed = self.client.get("/vms", headers=self.headers) detail = self.client.get("/vms/test-vm", headers=self.headers) self.assertEqual(listed.status_code, 200) self.assertEqual(detail.status_code, 200) self.assertIsInstance(listed.json(), list) self.assertEqual(listed.json(), [detail.json()]) body = detail.json() self.assertEqual(set(body), expected_fields) self.assertEqual(body["name"], "test-vm") self.assertEqual(body["status"], "running") self.assertEqual(body["observed_status"], "dead") self.assertEqual(body["policy_status"], policy_status) self.assertEqual(body["operation"], "ports") self.assertEqual(body["ports"], None if ports is None else [p.to_dict() for p in ports]) self.assertNotIn("guest-secret", detail.text) def test_get_delegates_id_ip_and_name_resolution_to_inspect(self) -> None: with ( patch.object(self.lifecycle, "inspect", wraps=self.lifecycle.inspect) as inspect, patch.object(self.lifecycle, "list_vms", side_effect=AssertionError("must use inspect")), ): for identifier in ("vm-test", "10.42.0.2", "test-vm"): with self.subTest(identifier=identifier): response = self.client.get(f"/vms/{identifier}", headers=self.headers) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["id"], "vm-test") inspect.assert_called_with(identifier) def test_stop_and_destroy_pass_identifiers_to_lifecycle(self) -> None: with ( patch.object(self.lifecycle, "stop", wraps=self.lifecycle.stop) as stop, patch.object(self.lifecycle, "destroy", wraps=self.lifecycle.destroy) as destroy, ): for identifier in ("vm-test", "10.42.0.2", "test-vm"): with self.subTest(identifier=identifier): stopped = self.client.post(f"/vms/{identifier}/stop", headers=self.headers) self.assertEqual(stopped.status_code, 200) self.assertEqual(stopped.json()["id"], "vm-test") self.assertEqual(stopped.json()["policy_status"], "inactive") self.assertEqual(stopped.json()["ports"], [{"port": 22, "protocol": "TCP"}]) stop.assert_called_with(identifier) destroyed = self.client.delete(f"/vms/{identifier}", headers=self.headers) self.assertEqual(destroyed.status_code, 200) self.assertEqual(destroyed.json()["id"], "vm-test") destroy.assert_called_with(identifier) def test_lifecycle_timeouts_are_forwarded_when_supplied(self) -> None: with ( patch.object(self.lifecycle, "restart", wraps=self.lifecycle.restart) as restart, patch.object(self.lifecycle, "stop", wraps=self.lifecycle.stop) as stop, patch.object(self.lifecycle, "destroy", wraps=self.lifecycle.destroy) as destroy, ): self.assertEqual( self.client.post("/vms/vm-test/restart?timeout=1.5", headers=self.headers).status_code, 200, ) self.assertEqual( self.client.post("/vms/vm-test/stop?timeout=2.5", headers=self.headers).status_code, 200, ) self.assertEqual( self.client.delete("/vms/vm-test?timeout=3.5", headers=self.headers).status_code, 200, ) restart.assert_called_once_with("vm-test", timeout=1.5) stop.assert_called_once_with("vm-test", timeout=2.5) destroy.assert_called_once_with("vm-test", timeout=3.5) def test_start_restart_and_port_routes_update_vm_state(self) -> None: started = self.client.post("/vms/vm-test/start", headers=self.headers) self.assertEqual(started.status_code, 200) self.assertEqual(started.json()["status"], "running") self.assertEqual(started.json()["policy_status"], "enforced") restarted = self.client.post("/vms/vm-test/restart", headers=self.headers) self.assertEqual(restarted.status_code, 200) self.assertEqual(restarted.json()["status"], "running") self.lifecycle.vm.status = "stopped" self.lifecycle.policy_status = "inactive" sync = self.client.post("/vms/vm-test/ports/sync", headers=self.headers) self.assertEqual(sync.status_code, 200) self.assertEqual(sync.json()["id"], "vm-test") self.assertEqual(sync.json()["policy_status"], "inactive") def test_exec_preserves_script_text_privileges_and_nonzero_exit_results(self) -> None: command = "printf 'hello\\n'\nprintf 'problem\\n' >&2\nexit 7" response = self.client.post("/vms/vm-test/exec", headers=self.headers, json={"command": command, "sudo": True, "timeout_seconds": 12.5}) self.assertEqual(response.status_code, 200, response.text) self.assertEqual(response.json(), { "exit_code": 7, "stdout": "hello\n", "stderr": "problem\n", "timed_out": False, "truncated": False, }) self.assertEqual(self.lifecycle.last_execution, { "vm_id": "vm-test", "command": command, "sudo": True, "timeout_seconds": 12.5, }) self.assertNotIn("guest-secret", response.text) default = self.client.post("/vms/vm-test/exec", headers=self.headers, json={"command": "id"}) self.assertEqual(default.status_code, 200) self.assertEqual(self.lifecycle.last_execution["sudo"], False) self.assertEqual(self.lifecycle.last_execution["timeout_seconds"], 60) def test_exec_rejects_malformed_requests_before_calling_the_service(self) -> None: for payload in ( {}, {"command": ""}, {"command": " \n"}, {"command": "a\0b"}, {"command": 42}, {"command": "x" * 65537}, {"command": "\u00e9" * 40000}, {"command": "id", "sudo": "false"}, {"command": "id", "sudo": 1}, {"command": "id", "timeout_seconds": True}, {"command": "id", "timeout_seconds": "10"}, {"command": "id", "timeout_seconds": 0}, {"command": "id", "timeout_seconds": 3601}, {"command": "id", "username": "root"}, {"command": "id", "host_path": "/etc/passwd"}, ): with self.subTest(payload=str(payload)[:100]): response = self.client.post("/vms/vm-test/exec", headers=self.headers, json=payload) self.assertEqual(response.status_code, 422, response.text) self.assertIsNone(self.lifecycle.last_execution) def test_exec_errors_distinguish_vm_state_agent_readiness_and_unknown_outcomes(self) -> None: for error, expected in ((NotFoundError("missing"), 404), (ConflictError("stopped"), 409), (GuestAgentUnavailableError("not ready"), 503), (GuestExecutionError("lost response", outcome_unknown=True), 502)): with self.subTest(error=error), patch.object(self.lifecycle, "execute", side_effect=error): response = self.client.post("/vms/vm-test/exec", headers=self.headers, json={"command": "id"}) self.assertEqual(response.status_code, expected) if isinstance(error, GuestExecutionError): self.assertTrue(response.json()["error"]["outcome_unknown"]) with patch.object(self.lifecycle, "execute", return_value=ExecResult(None, "partial", "", True, True)): response = self.client.post("/vms/vm-test/exec", headers=self.headers, json={"command": "sleep 10"}) self.assertEqual(response.status_code, 200) self.assertIsNone(response.json()["exit_code"]) self.assertTrue(response.json()["timed_out"]) self.assertTrue(response.json()["truncated"]) def test_exec_rejects_nonfinite_and_invalid_unicode_json_without_serialization_errors(self) -> None: for body in ('{"command":"id","timeout_seconds":NaN}', '{"command":"id","timeout_seconds":Infinity}', '{"command":"\\ud800"}', '{"command":"id","sudo":NaN}'): with self.subTest(body=body): response = self.client.post("/vms/vm-test/exec", headers={**self.headers, "Content-Type": "application/json"}, content=body) self.assertEqual(response.status_code, 422, response.text) self.assertIsNone(self.lifecycle.last_execution) def test_agent_install_requires_stopping_and_uses_the_existing_vm_disk(self) -> None: blocked = self.client.post("/vms/vm-test/agent/install", headers=self.headers) self.assertEqual(blocked.status_code, 409) self.client.post("/vms/vm-test/stop", headers=self.headers) with patch.object(self.lifecycle, "install_guest_agent", wraps=self.lifecycle.install_guest_agent) as install: response = self.client.post("/vms/vm-test/agent/install", headers=self.headers) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"id": "vm-test", "status": "installed", "restart_required": True}) install.assert_called_once_with("vm-test") self.assertEqual(self.lifecycle.vm.status, "stopped") def test_bulk_action_resolves_targets_before_running_fixed_ids(self) -> None: with ( patch.object(self.lifecycle, "resolve_targets", wraps=self.lifecycle.resolve_targets) as resolve, patch.object(self.lifecycle, "run_many", wraps=self.lifecycle.run_many) as run_many, ): response = self.client.post( "/vms/actions", headers=self.headers, json={"action": "restart", "identifiers": ["test-vm"], "timeout": 1.5}, ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [{"id": "vm-test", "action": "restart", "outcome": "succeeded", "error": None}]) resolve.assert_called_once_with(["test-vm"], all_vms=False) run_many.assert_called_once_with("restart", ["vm-test"], timeout=1.5) def test_bulk_action_requires_exactly_one_selection_strategy(self) -> None: for payload in ( {"action": "start"}, {"action": "start", "identifiers": ["vm-test"], "all_vms": True}, ): with self.subTest(payload=payload): response = self.client.post("/vms/actions", headers=self.headers, json=payload) self.assertEqual(response.status_code, 422) def test_log_and_diagnostic_routes_return_bounded_observability_data(self) -> None: logs = self.client.get("/vms/test-vm/logs?tail=5", headers=self.headers) diagnostics = self.client.get("/diagnostics", headers=self.headers) self.assertEqual(logs.status_code, 200) self.assertEqual(logs.json(), {"content": "last 5 log lines", "truncated": False}) self.assertEqual(diagnostics.status_code, 200) self.assertEqual(diagnostics.json(), [{"name": "kvm", "ok": True, "message": "available"}]) def test_port_update_modes_mutate_rules(self) -> None: set_resp = self.client.post( "/vms/vm-test/ports", headers=self.headers, json={"mode": "set", "ports": [{"port": 80, "protocol": "HTTP"}, {"port": 443, "protocol": "HTTPS"}]}, ) self.assertEqual(set_resp.status_code, 200) self.assertEqual(set_resp.json()["ports"], [{"port": 80, "protocol": "HTTP"}, {"port": 443, "protocol": "HTTPS"}]) open_resp = self.client.post( "/vms/vm-test/ports", headers=self.headers, json={"mode": "open", "ports": [{"port": 22, "protocol": "TCP"}]}, ) self.assertEqual(open_resp.status_code, 200) self.assertEqual(open_resp.json()["ports"], [{"port": 22, "protocol": "TCP"}, {"port": 80, "protocol": "HTTP"}, {"port": 443, "protocol": "HTTPS"}]) close_resp = self.client.post( "/vms/vm-test/ports", headers=self.headers, json={"mode": "close", "ports": [{"port": 80, "protocol": "HTTP"}]}, ) self.assertEqual(close_resp.status_code, 200) self.assertEqual(close_resp.json()["ports"], [{"port": 22, "protocol": "TCP"}, {"port": 443, "protocol": "HTTPS"}]) def test_create_claims_enforcement_only_for_successful_managed_vms(self) -> None: for ports, status, operation, policy_status in ( (None, "running", None, "unmanaged"), ((), "running", None, "enforced"), ((PortRule(22),), "failed", None, "unknown"), ((PortRule(22),), "starting", "create", "unknown"), ((PortRule(22),), "running", "ports", "unknown"), ): with self.subTest(ports=ports, status=status, operation=operation): self.lifecycle.vm.ports = ports self.lifecycle.vm.status = status self.lifecycle.vm.operation = operation with patch.object(self.lifecycle, "create", return_value=self.lifecycle.vm): response = self.client.post("/vms", headers=self.headers, json={}) self.assertEqual(response.status_code, 201) self.assertEqual(response.json()["policy_status"], policy_status) def test_lifecycle_errors_use_typed_status_codes(self) -> None: for method, path, operation in ( ("GET", "/vms", "list_vms"), ("GET", "/vms/test-vm", "inspect"), ("GET", "/vms/test-vm/logs", "read_log_tail"), ("POST", "/vms", "create"), ("POST", "/vms/test-vm/start", "start"), ("POST", "/vms/test-vm/restart", "restart"), ("POST", "/vms/test-vm/stop", "stop"), ("DELETE", "/vms/test-vm", "destroy"), ("POST", "/vms/test-vm/ports", "set_ports"), ("POST", "/vms/test-vm/ports/sync", "sync_ports"), ("GET", "/diagnostics", "diagnostics"), ): for error, expected in ( (NotFoundError("unknown guest"), 404), (ConflictError("ambiguous name"), 409), (ValidationError("invalid request"), 422), (ConfigurationError("invalid settings"), 422), (UvmError("this operation needs root"), 403), (UvmError("host operation failed"), 500), ): with self.subTest(operation=operation, error=error): with patch.object(self.lifecycle, operation, side_effect=error): response = self.client.request(method, path, headers=self.headers, json={}) self.assertEqual(response.status_code, expected) self.assertEqual(response.json(), {"error": {"message": str(error)}}) def test_missing_vm_returns_not_found(self) -> None: response = self.client.get("/vms/missing", headers=self.headers) self.assertEqual(response.status_code, 404) self.assertEqual(response.json(), {"error": {"message": "VM not found: missing"}}) def test_api_schema_exposes_management_operations(self) -> None: schema = self.client.get("/openapi.json").json() self.assertEqual( {(path, method) for path, methods in schema["paths"].items() for method in methods}, { ("/health", "get"), ("/exit", "post"), ("/install", "post"), ("/vms", "get"), ("/vms", "post"), ("/vms/actions", "post"), ("/vms/{identifier}", "get"), ("/vms/{identifier}/logs", "get"), ("/vms/{vm_id}/start", "post"), ("/vms/{vm_id}/restart", "post"), ("/vms/{vm_id}/resize", "post"), ("/vms/{vm_id}/stop", "post"), ("/vms/{vm_id}", "delete"), ("/vms/{vm_id}/ports", "post"), ("/vms/{vm_id}/ports/sync", "post"), ("/vms/{vm_id}/exec", "post"), ("/vms/{vm_id}/agent/install", "post"), ("/tools", "get"), ("/vms/{vm_id}/tools", "get"), ("/vms/{vm_id}/tools/install", "post"), ("/vms/{vm_id}/tools/jobs/{job_id}", "get"), ("/diagnostics", "get"), }, ) self.assertEqual( set(schema["components"]["schemas"]["PortRuleModel"]["properties"]), {"port", "protocol"}, ) def test_install_route_uses_the_existing_installer(self) -> None: response = self.client.post("/install", headers=self.headers, json={"force": True}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["firecracker"], "/tmp/firecracker")