From fb80ce7c8a730aa9ccedbf378d013be6db66144d Mon Sep 17 00:00:00 2001 From: kstyagi Date: Mon, 21 Sep 2026 19:16:33 +0530 Subject: [PATCH] feat: Add storage management for VM creation and resizing - Introduced storage parameter in VmCreateRequest and VmResizeRequest models. - Implemented storage validation to ensure minimum and maximum limits. - Enhanced CLI to support storage options during VM creation and resizing. - Updated lifecycle management to handle pending resize operations for VM resources. - Added methods for resizing VM disks and validating storage capacity. - Updated API endpoints to accommodate new storage functionality. - Refactored related validation and configuration logic to support storage management. --- DEVELOPER.md | 22 +++- README.md | 18 ++- USAGE.md | 32 ++++- tests/integration/test_private_ports.py | 2 +- tests/test_config.py | 70 +++++++++++ tests/test_firecracker_config.py | 27 +++++ tests/test_firewall.py | 95 ++++++++++++++- tests/test_install.py | 113 ++++++++++++++++++ uvm/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 214 bytes uvm/__pycache__/app.cpython-312.pyc | Bin 0 -> 1880 bytes uvm/__pycache__/cli.cpython-312.pyc | Bin 0 -> 37060 bytes uvm/__pycache__/config.cpython-312.pyc | Bin 0 -> 9864 bytes uvm/__pycache__/domain.cpython-312.pyc | Bin 0 -> 15467 bytes uvm/__pycache__/errors.cpython-312.pyc | Bin 0 -> 2818 bytes uvm/__pycache__/firewall.cpython-312.pyc | Bin 0 -> 37326 bytes uvm/__pycache__/images.cpython-312.pyc | Bin 0 -> 19772 bytes uvm/__pycache__/install.cpython-312.pyc | Bin 0 -> 13317 bytes uvm/__pycache__/integrity.cpython-312.pyc | Bin 0 -> 7099 bytes uvm/__pycache__/lifecycle.cpython-312.pyc | Bin 0 -> 42509 bytes uvm/__pycache__/network.cpython-312.pyc | Bin 0 -> 14849 bytes uvm/__pycache__/state.cpython-312.pyc | Bin 0 -> 9911 bytes uvm/__pycache__/system.cpython-312.pyc | Bin 0 -> 5103 bytes uvm/__pycache__/validation.cpython-312.pyc | Bin 0 -> 6574 bytes uvm/api_models.py | 19 +++ uvm/cli.py | 42 ++++++- uvm/config.py | 36 ++++-- uvm/domain.py | 50 +++++++- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 219 bytes .../__pycache__/api.cpython-312.pyc | Bin 0 -> 4854 bytes .../__pycache__/config.cpython-312.pyc | Bin 0 -> 3937 bytes .../__pycache__/process.cpython-312.pyc | Bin 0 -> 23877 bytes uvm/firecracker/config.py | 2 + uvm/firewall.py | 50 +++++--- uvm/images.py | 84 +++++++++++-- uvm/install.py | 18 +-- uvm/lifecycle.py | 105 ++++++++++++---- uvm/routers/vms.py | 23 ++++ uvm/validation.py | 18 ++- 38 files changed, 728 insertions(+), 98 deletions(-) create mode 100644 tests/test_config.py create mode 100644 tests/test_install.py create mode 100644 uvm/__pycache__/__init__.cpython-312.pyc create mode 100644 uvm/__pycache__/app.cpython-312.pyc create mode 100644 uvm/__pycache__/cli.cpython-312.pyc create mode 100644 uvm/__pycache__/config.cpython-312.pyc create mode 100644 uvm/__pycache__/domain.cpython-312.pyc create mode 100644 uvm/__pycache__/errors.cpython-312.pyc create mode 100644 uvm/__pycache__/firewall.cpython-312.pyc create mode 100644 uvm/__pycache__/images.cpython-312.pyc create mode 100644 uvm/__pycache__/install.cpython-312.pyc create mode 100644 uvm/__pycache__/integrity.cpython-312.pyc create mode 100644 uvm/__pycache__/lifecycle.cpython-312.pyc create mode 100644 uvm/__pycache__/network.cpython-312.pyc create mode 100644 uvm/__pycache__/state.cpython-312.pyc create mode 100644 uvm/__pycache__/system.cpython-312.pyc create mode 100644 uvm/__pycache__/validation.cpython-312.pyc create mode 100644 uvm/firecracker/__pycache__/__init__.cpython-312.pyc create mode 100644 uvm/firecracker/__pycache__/api.cpython-312.pyc create mode 100644 uvm/firecracker/__pycache__/config.cpython-312.pyc create mode 100644 uvm/firecracker/__pycache__/process.cpython-312.pyc diff --git a/DEVELOPER.md b/DEVELOPER.md index 50d045b..961b339 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -49,7 +49,7 @@ Requirements for real host operations: - Root privileges for installation and lifecycle mutations - `iproute2`, `iptables`, `nftables`, `conntrack`, `e2fsprogs`, `util-linux`, `openssl`, and OpenSSH client tools -- An x86_64 host for the default kernel/rootfs URLs +- An x86_64 or ARM64 (`aarch64`) host, including NVIDIA DGX Spark Do not run real integration operations on a developer workstation unless the bridge, subnet, firewall changes, and guest workload are acceptable there. @@ -432,6 +432,8 @@ The `firecracker/` package has three separate responsibilities. The guest receives a static IP via the kernel `ip=` boot argument. The netmask comes from `Settings.network`, not a hard-coded `/24` value. +ARM64 guests also receive `keep_bootcon`, as recommended by Firecracker's +getting-started guide; both architectures use `console=ttyS0`. ### Process Safety @@ -563,6 +565,9 @@ exclusive create for its replacement. Foreign ownership is an error, not an adoption opportunity. Readback normalizes presentation differences but compares all effective objects, ordered rules, hooks, identity, and the full port set; rule counts or an owner comment alone are not proof of enforcement. +Normalization materializes implied EtherType constraints before folding +transport-protocol dependencies. This accepts nftables' omitted redundant +`ether type ip` checks while retaining the IPv4 scope of TCP rules. The policy has prerouting/input/forward/output hooks. Early prerouting identity checks run at priority -310, before conntrack and bridge MAC learning; input @@ -590,6 +595,8 @@ of conntrack entries scoped to the guest IP in all four original/reply source and destination fields, including NAT reply tuples. Only then may teardown release the address. Rule removal also requires an absent TAP and verified table ownership; it never flushes global firewall or conntrack state. +Cleanup uses conntrack's documented `-D` (delete) and `-L` (list) commands, +including on conntrack-tools 1.4.8, where `--list` is not a supported option. ### Coexistence and Recovery @@ -612,6 +619,15 @@ do not import its unimplemented topology checks into current documentation. `Installer` is the only owner of apt installation, Firecracker artifact handling, downloads, and integrity-manifest publication. +`Settings` detects and normalizes the host architecture at construction time +(`amd64` to `x86_64`, `arm64` to `aarch64`) and rejects unsupported architectures. +The same architecture selects the Firecracker release archive, guest kernel, +rootfs, and architecture-specific boot arguments. Default guest URLs use +Firecracker's `img/quickstart_guide//` assets: `kernels/vmlinux.bin` +and `rootfs/bionic.rootfs.ext4`. Kernel and rootfs URL overrides are independent; +custom assets and trusted digests must match the host architecture. ARM64 +uses an uncompressed Linux `Image` (PE format) and an ARM64 ext4 rootfs. + The host package list includes `nftables` and `conntrack` as well as the existing iproute2/iptables, ext4, OpenSSL, OpenSSH, download, and util-linux tools. UVM adds no reconciliation daemon or tested external firewall-manager @@ -788,6 +804,10 @@ prove the kernel's actual packet behavior. FastAPI route tests are conditionally enabled when both `fastapi` and `httpx` are installed. They should be enabled in CI using the `test` extra. +When `nft` and `conntrack` are installed, command-parser tests append an invalid +final command/option so parsing is checked without executing any mutation. +These checks supplement fixture-based readback and cleanup tests; they do not +exercise the kernel packet path. ### Isolated Packet Tests diff --git a/README.md b/README.md index 506024e..1406719 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The project is an application, not a reusable Python SDK. - Root access for UVM commands that read or mutate the protected VM registry - KVM enabled and accessible at `/dev/kvm` - Linux 5.3 or newer with native bridge conntrack and safe pidfd support -- An x86_64 host for the default guest assets +- An x86_64 or ARM64 (`aarch64`) host, including NVIDIA DGX Spark - Working host networking, `iproute2`, `iptables`, `nftables`, `conntrack`, `e2fsprogs`, `util-linux`, `openssl`, and `openssh-client` - An STP-disabled software bridge; no offload path that bypasses bridge hooks @@ -48,10 +48,17 @@ before downloading VM artifacts. Boot operations additionally check that the native nftables bridge policy is supported; installing packages alone is not a connectivity or isolation test. +Firecracker, the guest kernel, and the Ubuntu rootfs are selected automatically +for the host architecture. On NVIDIA DGX Spark, UVM uses the ARM64 assets. +`arm64` and `amd64` architecture names are normalized to `aarch64` and +`x86_64`, respectively. Custom `UVM_KERNEL_URL` and `UVM_ROOTFS_URL` overrides +must point to assets compatible with the host architecture. + ## Trusted Installation Installation requires trusted SHA-256 digests by default. Supply digests from -an independently trusted source before running the privileged installer: +an independently trusted source for the selected architecture before running +the privileged installer: ```sh sudo env \ @@ -407,8 +414,8 @@ Host and API settings can be overridden through environment variables: | `UVM_NETWORK` | Guest private IPv4 CIDR; defaults to `10.42.0.0/24` | | `UVM_GATEWAY` | Bridge/guest gateway; defaults to `10.42.0.1` | | `UVM_BRIDGE` | Linux bridge name; defaults to `uvm0` | -| `UVM_KERNEL_URL` | Guest kernel download URL | -| `UVM_ROOTFS_URL` | Guest rootfs download URL | +| `UVM_KERNEL_URL` | Override the host-architecture default guest kernel URL | +| `UVM_ROOTFS_URL` | Override the host-architecture default guest rootfs URL | | `UVM_FIRECRACKER_SHA256` | Trusted Firecracker release archive SHA-256 | | `UVM_KERNEL_SHA256` | Trusted guest kernel SHA-256 | | `UVM_ROOTFS_SHA256` | Trusted guest rootfs SHA-256 | @@ -458,6 +465,9 @@ retained-disk resume, bulk operations, lifecycle rollback, dashboard delivery, and API routes. API route tests run when the FastAPI and `httpx` test dependencies are installed. +When `nft` and `conntrack` are installed, the suite also checks their command +parsers using deliberately invalid final commands/options that prevent mutations. + Opt-in packet checks run **only in disposable isolated user/network namespaces**, which may require privileges and the installed network tools: diff --git a/USAGE.md b/USAGE.md index d4e94ad..47d1a73 100644 --- a/USAGE.md +++ b/USAGE.md @@ -44,7 +44,7 @@ You need: - KVM enabled and readable/writable at `/dev/kvm`. - Linux >= 5.3 with native bridge conntrack and safe pidfd support. - Root access for installation and VM lifecycle operations. -- An x86_64 host for the default guest kernel/rootfs downloads. +- An x86_64 or ARM64 (`aarch64`) host, including NVIDIA DGX Spark. - Internet access if downloading Firecracker and guest assets. - `iproute2`, `iptables`, `nftables`, `conntrack`, `e2fsprogs`, `util-linux`, `openssl`, and `openssh-client`; `install` installs the host packages. @@ -140,6 +140,23 @@ UVM installs host packages, Firecracker, Jailer, a guest kernel, and a guest rootfs template. It requires SHA-256 checksums by default because it downloads artifacts that will be used by a privileged process. +### ARM64 Hosts (NVIDIA DGX Spark) + +UVM detects the host architecture and selects matching Firecracker, kernel, +and rootfs downloads. NVIDIA DGX Spark uses the `aarch64` assets; the `arm64` +architecture alias is also accepted. The default ARM64 guest downloads are: + +- Kernel: `https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/aarch64/kernels/vmlinux.bin` +- Rootfs: `https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/aarch64/rootfs/bionic.rootfs.ext4` + +Use the installation commands below with checksums for those ARM64 artifacts +and the ARM64 Firecracker release archive. `UVM_KERNEL_URL` and +`UVM_ROOTFS_URL` can override either download independently. Custom images must +match the host architecture and the [guest image contract](README.md#guest-image-contract). +On ARM64, the kernel must be an uncompressed ARM64 Linux `Image` (PE format), +and the rootfs must be an ARM64 ext4 filesystem. UVM adds `keep_bootcon` to the +guest boot arguments for ARM64 console output. + ### Scenario: Strict First Installation Obtain trusted checksums independently, then run: @@ -196,8 +213,7 @@ sudo env UVM_ALLOW_UNVERIFIED_DOWNLOADS=1 ./uvm.py start VM | `/dev/kvm does not exist` | Enable hardware/nested virtualization first. | | `checksum is required` | Provide the requested `UVM_*_SHA256` variable. | | `SHA-256 mismatch` | Stop and obtain the correct digest or artifact URL. | -| `unsupported host architecture` | Use x86_64 defaults or provide compatible assets. | -| `default guest assets support x86_64 only` | Configure both custom kernel and rootfs URLs for ARM. | +| `unsupported host architecture` | Use an x86_64 or ARM64 Linux host. | ## 5. Create VMs @@ -306,6 +322,12 @@ On failure, UVM attempts containment and cleanup. It does not release the disk or IP reservation if safe process exit or cleanup cannot be confirmed; inspect the retained `failed` record and `last_error` before retrying. +After updating UVM to fix a create/cleanup error, restart the API server to +load the updated code. To discard a failed creation and release its reserved +IP, run `sudo ./uvm.py delete VM -y` with that failed VM's exact ID, or send +an authenticated `DELETE /vms/VM`. Deletion retries the outstanding cleanup +and removes the failed VM's private disk. Then retry the create request. + ## 6. Inspect VMs ```sh @@ -938,8 +960,8 @@ automatically detects every conflict with host routes or other managers. | `UVM_NETWORK` | Guest subnet; default `10.42.0.0/24` | | `UVM_GATEWAY` | Guest gateway / bridge IP; default `10.42.0.1` | | `UVM_BRIDGE` | Linux bridge name; default `uvm0` | -| `UVM_KERNEL_URL` | Custom kernel download URL | -| `UVM_ROOTFS_URL` | Custom rootfs download URL | +| `UVM_KERNEL_URL` | Override the host-architecture default kernel URL | +| `UVM_ROOTFS_URL` | Override the host-architecture default rootfs URL | | `UVM_FIRECRACKER_SHA256` | Strict Firecracker archive checksum | | `UVM_KERNEL_SHA256` | Strict kernel checksum | | `UVM_ROOTFS_SHA256` | Strict rootfs checksum | diff --git a/tests/integration/test_private_ports.py b/tests/integration/test_private_ports.py index 32c77a2..263aa1c 100644 --- a/tests/integration/test_private_ports.py +++ b/tests/integration/test_private_ports.py @@ -296,7 +296,7 @@ def _sandbox(parent_fd): print("PASS: host/peer inbound services and host/peer/routed guest TCP egress", flush=True) def service_ids(): - listing = run("conntrack", "--list", "--family", "ipv4", "--proto", "tcp", + listing = run("conntrack", "-L", "--family", "ipv4", "--proto", "tcp", "--orig-dst", vm.guest_ip, "--dport", "8080", "-o", "id") return set(re.findall(r"\bid=(\d+)", listing)) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..f9b24d0 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import os +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +from uvm.config import Settings +from uvm.errors import ConfigurationError + + +class ArchitectureSettingsTests(unittest.TestCase): + def test_resolves_native_defaults_when_settings_are_created(self) -> None: + for machine, architecture in ( + ("x86_64", "x86_64"), + ("AMD64", "x86_64"), + ("aarch64", "aarch64"), + ("arm64", "aarch64"), + ): + for factory in (Settings, Settings.from_environment): + with ( + self.subTest(machine=machine, factory=factory), + patch.dict(os.environ, {}, clear=True), + patch("uvm.config.os.uname", return_value=SimpleNamespace(machine=machine)), + ): + settings = factory() + + base = ( + "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/" + f"{architecture}" + ) + self.assertEqual(settings.architecture, architecture) + self.assertEqual(settings.kernel_url, f"{base}/kernels/vmlinux.bin") + self.assertEqual(settings.rootfs_url, f"{base}/rootfs/bionic.rootfs.ext4") + + def test_preserves_individual_environment_url_overrides_on_arm64(self) -> None: + kernel = "https://example.com/custom-arm64-kernel" + rootfs = "https://example.com/custom-arm64-rootfs.ext4" + base = "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/aarch64" + for overrides in ( + {"UVM_KERNEL_URL": kernel}, + {"UVM_ROOTFS_URL": rootfs}, + {"UVM_KERNEL_URL": kernel, "UVM_ROOTFS_URL": rootfs}, + ): + with ( + self.subTest(overrides=overrides), + patch.dict(os.environ, overrides, clear=True), + patch("uvm.config.os.uname", return_value=SimpleNamespace(machine="aarch64")), + ): + settings = Settings.from_environment() + + self.assertEqual( + settings.kernel_url, + overrides.get("UVM_KERNEL_URL", f"{base}/kernels/vmlinux.bin"), + ) + self.assertEqual( + settings.rootfs_url, + overrides.get("UVM_ROOTFS_URL", f"{base}/rootfs/bionic.rootfs.ext4"), + ) + + def test_rejects_unsupported_hosts_even_with_custom_guest_assets(self) -> None: + with ( + patch.dict(os.environ, { + "UVM_KERNEL_URL": "https://example.com/kernel", + "UVM_ROOTFS_URL": "https://example.com/rootfs.ext4", + }, clear=True), + patch("uvm.config.os.uname", return_value=SimpleNamespace(machine="riscv64")), + self.assertRaisesRegex(ConfigurationError, "unsupported host architecture: riscv64"), + ): + Settings.from_environment() diff --git a/tests/test_firecracker_config.py b/tests/test_firecracker_config.py index 6097371..939cfe6 100644 --- a/tests/test_firecracker_config.py +++ b/tests/test_firecracker_config.py @@ -19,6 +19,33 @@ class RecordingFirecrackerClient(FirecrackerClient): class FirecrackerConfigTests(unittest.TestCase): + def test_uses_arm_boot_console_arguments_only_on_arm64(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", + ) + for architecture in ("x86_64", "aarch64", "arm64"): + with self.subTest(architecture=architecture): + config = build_config( + Settings(architecture=architecture), + vm, + Path("/images/vmlinux"), + Path("/vm/rootfs.ext4"), + ) + + arguments = config["boot-source"]["boot_args"].split() + self.assertEqual("keep_bootcon" in arguments, architecture != "x86_64") + self.assertIn("console=ttyS0", arguments) + self.assertIn("ip=10.42.0.2::10.42.0.1:255.255.255.0::eth0:off", arguments) + def test_builds_a_writable_vm_disk_configuration(self) -> None: vm = VmRecord( id="vm-test", diff --git a/tests/test_firewall.py b/tests/test_firewall.py index d84e5a3..0746b6c 100644 --- a/tests/test_firewall.py +++ b/tests/test_firewall.py @@ -102,9 +102,9 @@ class FakeRunner: self.after_batch() elif args == ("conntrack", "--version"): stdout = "conntrack v1.4.8 (conntrack-tools)" - elif args[:2] == ("conntrack", "--delete"): + elif args[:2] == ("conntrack", "-D"): code = self.ct_delete_code - elif args[:2] == ("conntrack", "--list"): + elif args[:2] == ("conntrack", "-L"): stdout = self.ct_remaining else: raise AssertionError(f"unexpected command: {args}") @@ -376,6 +376,34 @@ class FirewallManagerTests(unittest.TestCase): exprs[:-1] = reversed(exprs[:-1]) self.assertEqual(self.manager.status(self.vm), "enforced") + def test_apply_accepts_nft_eliding_ethertype_from_the_ipv4_tcp_guard(self): + self.vm.ports = (PortRule(22),) + + def elide_ethertype(): + for item in self.policy(): + expressions = item.get("rule", {}).get("expr", []) + if not any("&" in expr.get("match", {}).get("left", {}) for expr in expressions): + continue + # nft 1.0.9 keeps ip protocol tcp to bind TCP to IPv4 in a + # bridge table, but omits its redundant ether type ip check. + expressions[:] = [expr for expr in expressions if expr != { + "match": {"left": {"payload": {"protocol": "ether", "field": "type"}}, + "op": "==", "right": "ip"}, + }] + + self.runner.after_batch = elide_ethertype + self.manager.apply(self.vm) + self.assertEqual(self.manager.status(self.vm), "enforced") + + # Losing BOTH IPv4 constraints is a real policy change, not formatting. + for item in self.policy(): + expressions = item.get("rule", {}).get("expr", []) + if any("&" in expr.get("match", {}).get("left", {}) for expr in expressions): + expressions[:] = [expr for expr in expressions if expr.get("match", {}).get("left") != { + "payload": {"protocol": "ip", "field": "protocol"}, + }] + self.assertEqual(self.manager.status(self.vm), "drift") + def test_empty_sets_can_omit_elements_and_missing_conditions_are_drift(self): self.vm.ports = () self.manager.apply(self.vm) @@ -430,7 +458,7 @@ class FirewallManagerTests(unittest.TestCase): self.runner.ct_delete_code = 1 # conntrack uses 1 for no matching entries too. self.manager.cleanup_connections(self.vm) for selector in ("--orig-src", "--orig-dst", "--reply-src", "--reply-dst"): - for command in ("--delete", "--list"): + for command in ("-D", "-L"): self.assertIn(("conntrack", command, "--family", "ipv4", selector, self.vm.guest_ip), [args for args, _ in self.runner.calls]) self.assertFalse(any("--flush" in args or "-F" in args for args, _ in self.runner.calls)) @@ -438,7 +466,7 @@ class FirewallManagerTests(unittest.TestCase): with self.assertRaisesRegex(UvmError, "do not reuse"): self.manager.cleanup_connections(self.vm) self.runner.ct_remaining = "" - self.runner.fail_command = ("conntrack", "--list", "--family", "ipv4", "--orig-src", self.vm.guest_ip) + self.runner.fail_command = ("conntrack", "-L", "--family", "ipv4", "--orig-src", self.vm.guest_ip) with self.assertRaises(CommandError): self.manager.cleanup_connections(self.vm) @@ -455,7 +483,7 @@ class FirewallManagerTests(unittest.TestCase): expected = [("ip", "-j", "-d", "link", "show")] expected.extend(("conntrack", command, "--family", "ipv4", field, "203.0.113.7") for field in ("--orig-src", "--orig-dst", "--reply-src", "--reply-dst") - for command in ("--delete", "--list")) + for command in ("-D", "-L")) # No nft policy, publishing, or unrelated address/conntrack flush. self.assertEqual([args for args, _ in self.runner.calls], expected) @@ -476,7 +504,7 @@ class FirewallManagerTests(unittest.TestCase): with self.subTest(address=address, network=network): self.runner.calls.clear() self.manager.cleanup_connections(vm_record(guest_ip=address, network=network, ports=None)) - deletes = [args for args, _ in self.runner.calls if args[:2] == ("conntrack", "--delete")] + deletes = [args for args, _ in self.runner.calls if args[:2] == ("conntrack", "-D")] self.assertEqual(len(deletes), 4) self.assertTrue(all(args[-1] == address for args in deletes)) @@ -688,6 +716,32 @@ class NftDumpFixtureTests(unittest.TestCase): self.assertEqual(self.normalize_rule(dumped), self.normalize_rule(numeric)) self.assertNotEqual(self.normalize_rule(dumped), self.normalize_rule(numeric.replace('}}, 3]', '}}, 2]'))) + def test_bridge_tcp_guard_normalizes_numeric_protocol_and_implicit_ethertype(self): + # nft 1.0.9 src/payload.c payload_may_dependency_kill() removes the + # EtherType before ip protocol, but retains ip protocol before TCP. + # Protocol and flag representations follow the upstream fixtures in + # tests/py/inet/ip_tcp.t.json.output and tests/py/inet/tcp.t.json. + dumped = '''[ + {"match": {"left": {"payload": {"protocol": "ip", "field": "protocol"}}, + "op": "==", "right": 6}}, + {"match": {"left": {"ct": {"key": "state"}}, "op": "in", "right": "new"}}, + {"match": {"left": {"&": [ + {"payload": {"protocol": "tcp", "field": "flags"}}, ["fin", "syn", "rst", "ack"] + ]}, "op": "!=", "right": "syn"}}, + {"drop": null} + ]''' + policy = FirewallManager(Settings(), FakeRunner())._policy(vm_record(), quarantine=False) + guards = [item["rule"] for item in policy if "rule" in item + and any("&" in expr.get("match", {}).get("left", {}) for expr in item["rule"]["expr"])] + self.assertEqual({rule["chain"] for rule in guards}, {"from_vm", "to_vm"}) + for rule in guards: + with self.subTest(chain=rule["chain"]): + expected = self.normalize_rule(json.dumps(rule["expr"])) + self.assertEqual(expected, self.normalize_rule(dumped)) + self.assertNotEqual(expected, self.normalize_rule(json.dumps(json.loads(dumped)[1:]))) + self.assertNotEqual(expected, self.normalize_rule(dumped.replace('"right": 6', '"right": 17'))) + self.assertNotEqual(expected, self.normalize_rule(dumped.replace('"op": "!="', '"op": "=="'))) + def test_upstream_full_dump_ignores_only_handles_and_metainfo(self): # Literal RULESET from tests/shell/testcases/json/0006obj_comment_0. dumped = json.loads('''{"nftables": [ @@ -706,6 +760,35 @@ class NftDumpFixtureTests(unittest.TestCase): self.assertNotEqual(_normalize(dumped), _normalize(expected)) +@unittest.skipUnless(shutil.which("conntrack"), "conntrack unavailable for nonmutating option checks") +class ConntrackCommandParserTests(unittest.TestCase): + @staticmethod + def parse(command): + # An invalid final option aborts before any netlink operation, even + # when the tests run as root. This also checks delete command syntax. + return subprocess.run( + (*command, "--uvm-parser-sentinel"), text=True, capture_output=True, + timeout=5, env={**os.environ, "LC_ALL": "C"}, + ) + + def test_cleanup_commands_reach_only_the_final_invalid_option(self): + runner = FakeRunner() + FirewallManager(Settings(), runner).cleanup_connections(vm_record()) + commands = [args for args, _ in runner.calls if args[0] == "conntrack"] + self.assertEqual(len(commands), 8) + for command in commands: + with self.subTest(command=command): + result = self.parse(command) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertRegex(result.stderr, r"(?:unknown|unrecognized) option [`']--uvm-parser-sentinel'") + + def test_invalid_option_is_rejected_before_the_sentinel(self): + result = self.parse(("conntrack", "--uvm-invalid-option")) + self.assertEqual(result.returncode, 2) + self.assertIn("--uvm-invalid-option", result.stderr) + self.assertNotIn("--uvm-parser-sentinel", result.stderr) + + @unittest.skipUnless(shutil.which("nft"), "nft unavailable for noncommitting JSON parser checks") class NftJsonParserTests(unittest.TestCase): """Exercise the actual JSON parser, NOT semantic/kernel/packet validation. diff --git a/tests/test_install.py b/tests/test_install.py new file mode 100644 index 0000000..2576a15 --- /dev/null +++ b/tests/test_install.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import hashlib +import io +import os +import tarfile +import tempfile +import unittest +from dataclasses import replace +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from uvm.config import Settings +from uvm.errors import UvmError +from uvm.install import Installer +from uvm.integrity import load_manifest +from uvm.state import StateStore +from uvm.system import CommandRunner + + +class InstallerArchitectureTests(unittest.TestCase): + def test_installs_matching_native_artifacts_with_a_verified_manifest(self) -> None: + for machine, architecture in ( + ("x86_64", "x86_64"), + ("aarch64", "aarch64"), + ("arm64", "aarch64"), + ): + with ( + self.subTest(machine=machine), + tempfile.TemporaryDirectory() as temporary_directory, + patch.dict(os.environ, {"UVM_BASE": temporary_directory}, clear=True), + patch("uvm.config.os.uname", return_value=SimpleNamespace(machine=machine)), + ): + settings = Settings.from_environment() + version = settings.firecracker_version + binaries = { + "firecracker": f"firecracker for {architecture}".encode(), + "jailer": f"jailer for {architecture}".encode(), + } + archive_buffer = io.BytesIO() + with tarfile.open(fileobj=archive_buffer, mode="w:gz") as archive: + for name, content in binaries.items(): + member = tarfile.TarInfo( + f"release-{version}-{architecture}/{name}-{version}-{architecture}" + ) + member.size = len(content) + member.mode = 0o755 + archive.addfile(member, io.BytesIO(content)) + + archive_bytes = archive_buffer.getvalue() + kernel_bytes = f"kernel for {architecture}".encode() + rootfs_bytes = f"rootfs for {architecture}".encode() + settings = replace( + settings, + firecracker_sha256=hashlib.sha256(archive_bytes).hexdigest(), + kernel_sha256=hashlib.sha256(kernel_bytes).hexdigest(), + rootfs_sha256=hashlib.sha256(rootfs_bytes).hexdigest(), + ) + guest_base = ( + "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/" + f"{architecture}" + ) + payloads = { + "https://github.com/firecracker-microvm/firecracker/releases/download/" + f"{version}/firecracker-{version}-{architecture}.tgz": archive_bytes, + f"{guest_base}/kernels/vmlinux.bin": kernel_bytes, + f"{guest_base}/rootfs/bionic.rootfs.ext4": rootfs_bytes, + } + runner = Mock(spec=CommandRunner) + installer = Installer(settings, runner, StateStore(settings), emit=None) + with ( + patch("uvm.install.require_root"), + patch("uvm.install.check_kvm"), + patch( + "uvm.install.urllib.request.urlopen", + side_effect=lambda url, timeout: io.BytesIO(payloads[url]), + ) as download, + ): + firecracker, assets = installer.install() + + self.assertEqual(firecracker.read_bytes(), binaries["firecracker"]) + self.assertEqual(settings.jailer_binary.read_bytes(), binaries["jailer"]) + self.assertEqual(assets.kernel.read_bytes(), kernel_bytes) + self.assertEqual(assets.rootfs.read_bytes(), rootfs_bytes) + self.assertEqual(download.call_count, 3) + self.assertEqual({call.args[0] for call in download.call_args_list}, set(payloads)) + manifest = load_manifest(settings.integrity_manifest_path) + self.assertTrue(manifest.verified) + self.assertEqual(manifest.checksums, { + name: hashlib.sha256(content).hexdigest() + for name, content in { + **binaries, "kernel": kernel_bytes, "rootfs": rootfs_bytes, + }.items() + }) + + def test_arm64_install_requires_checksums_before_host_commands_or_downloads(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + settings = Settings(base=Path(temporary_directory), architecture="aarch64") + runner = Mock(spec=CommandRunner) + installer = Installer(settings, runner, StateStore(settings), emit=None) + with ( + patch("uvm.install.require_root"), + patch("uvm.install.check_kvm") as check_kvm, + patch("uvm.install.urllib.request.urlopen") as download, + self.assertRaisesRegex(UvmError, "Firecracker archive checksum is required"), + ): + installer.install() + + runner.run.assert_not_called() + check_kvm.assert_not_called() + download.assert_not_called() + self.assertFalse(settings.integrity_manifest_path.exists()) diff --git a/uvm/__pycache__/__init__.cpython-312.pyc b/uvm/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ce4397c9ce2b2484b557815f3b9f10fa41cacee GIT binary patch literal 214 zcmXv|yA8rH6tww9NZG-Z6yOCA9R&h00F7nE4`T37i4zg#K%%8(7B)!B0!VaJI0&w} zJL%rLyT|bW1$58X{VMRIF@NBnctt^6kU$$$;Zf@}Uldua`EJUQ?k zkgd&#Pk|JrCW+OW8!=YW0O8IlnS8^O^$6E}#)M56yP;Rp^&ZmHYM^3w6i`wFxu@X= jWlS3B8JoDOj;7j*LcuKSg^Oll-pqTpF~&7oW}RR@%QHMQ literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/app.cpython-312.pyc b/uvm/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb8baf7cc701b3efd55abe4d61cbdb2b4d1b82fd GIT binary patch literal 1880 zcmYjR&2QX96d!wSuXi{5kxiP9CMBk&AB!M?Yg>?_pq3~lf(9hysCmC7KzxUzincsVFeyUc> z1eX5o_Wk#CLjFJ{U!dbsj(!07g!sfJzT&H)5~;STATET3NV7GGwNQ@?+W@TlMp%qY z+myH%mZGvp{(U3hPaVe}uHM=HpIjlzwyCHEU9FCfHQ{tg;BpS6xC9Z~NqA`0+ zAu6GUUwdBPQRMbOze)VYW8x3*sF`Iadl7q_PP7$&)Nel4?a7Ui)|p51^PNr@c&-SN z*z%I7lkh-dmL$SzC(IJ}sMTGM&>6Y0|8^L})S?fm*A?z+NM~PR@|KcT+&E5zOvFJo z7u+z!0Kg^R6|NV$oWH_7qIZP~5yWd4Ie8;s)MKvqfU=KS;!(~&a$|RmGH{j`BS0%6 zVKUKT%mw6?uEwpPO}z~-q$`xI2Ob90mZ|tMVGm9vSb;qqC#WuDr?}mXW6C~-FLWLz zf_npooY;-13x@Dv@# zPdqT!9p_Y89p^i81R2SH({uNdh|c+I4}H2m7gH|gphxE1PG`2W!GtIQHEiv^56h zD{t{lMGVH2kRExi0@5EXVr4wsU%?D;v6UdxgJ>`^L7s&a*ew^D7^%HBa z(9?Q)&)6C4W3wob54kWG_;FuaX4y2w$iPX!Tj-g+;&V0ku8>w?8Jbk1Q6O3xLp~YC zvr!ahK(xwPS2Wrzi5wcQ2P}yr8Vgp#5Zo@qGqNERCJH>7>=ap}R*B7^0duDYlY=4u zks1Nc9#4yLHdJY`lZ~hRvD^a3)KS{zJr zRxK=dR=f*{u%K{#;q+%Wfx$Pppix$M?739$umCy%>1R_Y>t%otV06g Vs6f>5L#Z~&^?BuQQd2Uv{|9lE)vo{m literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/cli.cpython-312.pyc b/uvm/__pycache__/cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81b951168273d679aed7e95247fe14f9058062d6 GIT binary patch literal 37060 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 literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/config.cpython-312.pyc b/uvm/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43272c1efd1dc4524a28e759b337c4a64fc1649d GIT binary patch literal 9864 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 N&0m|%bTyF&{}0k`;n@HH literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/errors.cpython-312.pyc b/uvm/__pycache__/errors.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0033ff341aeca8a433ee804217c995f7224b210 GIT binary patch literal 2818 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 T{+Ofi)1#)#Jl`ds)c5}ZKfQNS literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/images.cpython-312.pyc b/uvm/__pycache__/images.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c0f05211b702683a37bf20bd6cefb0bd3528b0c GIT binary patch literal 19772 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 literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/integrity.cpython-312.pyc b/uvm/__pycache__/integrity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed2fe4242b910f147d6d78a80c3636f0b4cb4cfa GIT binary patch literal 7099 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 literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/lifecycle.cpython-312.pyc b/uvm/__pycache__/lifecycle.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f21e6fae4f1e2ddfdc197e9e275c731b83eba65b GIT binary patch literal 42509 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 literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/state.cpython-312.pyc b/uvm/__pycache__/state.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c452f93a52ae8b022a6050a3bec3e95a1bc8e5e GIT binary patch literal 9911 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 literal 0 HcmV?d00001 diff --git a/uvm/__pycache__/system.cpython-312.pyc b/uvm/__pycache__/system.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be6d5a28ec75cd47d30439362dee75a3ef29bf5d GIT binary patch literal 5103 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 literal 0 HcmV?d00001 diff --git a/uvm/api_models.py b/uvm/api_models.py index 9ccf2ed..aee7751 100644 --- a/uvm/api_models.py +++ b/uvm/api_models.py @@ -40,6 +40,7 @@ class VmCreateRequest(BaseModel): cpu: float = Field(default=1, gt=0) ram: str | int | float = "512" + storage: str | int | float = Field(default="30G", description="Disk capacity, minimum 30 GiB; bare numbers use GiB") guest_ip: IPv4Address | None = None username: str = "root" password: SecretStr = SecretStr("root") @@ -48,11 +49,27 @@ class VmCreateRequest(BaseModel): no_default_ports: bool = Field(default=False, strict=True) +class VmResizeRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + cpu: float | None = Field(default=None, gt=0, allow_inf_nan=False, strict=True) + ram: str | int | float | None = None + storage: str | int | float | None = Field(default=None, description="Disk capacity, minimum 30 GiB; bare numbers use GiB") + + +class VmResourcesModel(BaseModel): + cpu: float + ram_mib: int + storage_mib: int + + class VmResponse(BaseModel): id: str name: str | None cpu: float ram_mib: int + storage_mib: int | None + pending_resize: VmResourcesModel | None guest_ip: str gateway: str mac: str @@ -124,6 +141,8 @@ def vm_response( name=vm.name, cpu=vm.cpu, ram_mib=vm.ram_mib, + storage_mib=vm.storage_mib, + pending_resize=VmResourcesModel(**vm.pending_resize.to_dict()) if vm.pending_resize is not None else None, guest_ip=vm.guest_ip, gateway=vm.gateway, mac=vm.mac, diff --git a/uvm/cli.py b/uvm/cli.py index 413aa3b..b67e34d 100644 --- a/uvm/cli.py +++ b/uvm/cli.py @@ -22,6 +22,7 @@ from .validation import ( parse_password, parse_ports, parse_ram, + parse_storage, parse_timeout, parse_username, ) @@ -97,6 +98,10 @@ def build_parser(application: Application) -> argparse.ArgumentParser: default=str(application.settings.default_ram_mib), help="RAM, e.g. 512, 1G, 512M", ) + create.add_argument( + "--storage", default=f"{application.settings.default_storage_mib}M", + help="private disk capacity, e.g. 30G, 50G, 1T; minimum 30 GiB (bare numbers use GiB)", + ) create.add_argument( "--guest-ip", "--host-ip", @@ -130,6 +135,14 @@ def build_parser(application: Application) -> argparse.ArgumentParser: ) create.add_argument("--json", action="store_true") + resize = subcommands.add_parser("resize", help="stop, resize CPU/RAM/storage, and boot the retained VM") + resize.add_argument("vm", help="VM ID, name, or guest IP") + resize.add_argument("--cpu", help="new CPU capacity; omitted values are retained") + resize.add_argument("--ram", help="new RAM, e.g. 512M or 4G") + resize.add_argument("--storage", help="new private disk capacity, e.g. 30G or 50G; minimum 30 GiB") + resize.add_argument("--timeout", help="seconds to wait after SIGTERM before SIGKILL (normally 5s)") + resize.add_argument("--json", action="store_true") + listing = subcommands.add_parser("list", help="list VMs") listing.add_argument("--status", action="append", default=[], help="observed status (repeatable)") output = listing.add_mutually_exclusive_group() @@ -293,6 +306,7 @@ def _run_command(application: Application, args: argparse.Namespace) -> int: VmSpec( cpu=parse_cpu(args.cpu), ram_mib=parse_ram(args.ram), + storage_mib=parse_storage(args.storage), guest_ip=guest_ip, username=parse_username(args.username), password=parse_password(password), @@ -321,6 +335,7 @@ def _run_command(application: Application, args: argparse.Namespace) -> int: print(f" Name: {vm.name}") print(f" IP: {vm.guest_ip}") print(f" RAM: {vm.ram_mib} MiB") + print(f" Storage: {vm.storage_mib} MiB") print(f" CPU: {vm.cpu}") print(f" TAP: {vm.tap}") print(f" Username: {vm.username}") @@ -329,6 +344,27 @@ def _run_command(application: Application, args: argparse.Namespace) -> int: print(f" SSH: ssh {vm.username}@{vm.guest_ip}") return 0 + if args.command == "resize": + if args.cpu is None and args.ram is None and args.storage is None: + raise ValidationError("resize requires at least one of --cpu, --ram, or --storage") + vm = application.lifecycle.resize( + args.vm, + cpu=parse_cpu(args.cpu) if args.cpu is not None else None, + ram_mib=parse_ram(args.ram) if args.ram is not None else None, + storage_mib=parse_storage(args.storage) if args.storage is not None else None, + timeout=parse_timeout(args.timeout) if args.timeout is not None else None, + ) + if args.json: + _print_json(_vm_output(vm, policy_status="enforced")) + else: + print(f"VM resized and started: {vm.id}") + print(f" CPU: {vm.cpu}") + print(f" RAM: {vm.ram_mib} MiB") + print(f" Storage: {vm.storage_mib} MiB") + print(f" IP: {vm.guest_ip}") + print(f" Username: {vm.username}") + return 0 + if args.command == "list": vms = [ listed for listed in application.lifecycle.list_vms() @@ -349,14 +385,14 @@ def _run_command(application: Application, args: argparse.Namespace) -> int: return 0 print( f"{'ID':<37} {'NAME':<20} {'IP':<16} {'USER':<16} {'CPU':<7} " - f"{'RAM':<8} {'STATUS':<12} {'POLICY':<12} PID" + f"{'RAM':<8} {'DISK(MiB)':<12} {'STATUS':<12} {'POLICY':<12} PID" ) for listed in vms: vm = listed.vm pid = vm.pid if vm.pid is not None else "" print( f"{vm.id:<37} {vm.name or '-':<20} {vm.guest_ip:<16} {vm.username:<16} {vm.cpu:<7} " - f"{vm.ram_mib:<8} {listed.observed_status:<12} {listed.policy_status:<12} {pid}" + f"{vm.ram_mib:<8} {vm.storage_mib or '-':<12} {listed.observed_status:<12} {listed.policy_status:<12} {pid}" ) return 0 @@ -543,6 +579,8 @@ def _vm_output( "name": vm.name, "cpu": vm.cpu, "ram_mib": vm.ram_mib, + "storage_mib": vm.storage_mib, + "pending_resize": vm.pending_resize.to_dict() if vm.pending_resize is not None else None, "guest_ip": vm.guest_ip, "gateway": vm.gateway, "tap": vm.tap, diff --git a/uvm/config.py b/uvm/config.py index 81084b9..c36180e 100644 --- a/uvm/config.py +++ b/uvm/config.py @@ -3,21 +3,16 @@ from __future__ import annotations import os -from dataclasses import dataclass +from dataclasses import dataclass, field from ipaddress import IPv4Address, IPv4Network from pathlib import Path from .errors import ConfigurationError -DEFAULT_KERNEL_URL = ( - "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/" - "x86_64/kernels/vmlinux.bin" -) -DEFAULT_ROOTFS_URL = ( - "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/" - "x86_64/rootfs/bionic.rootfs.ext4" -) +DEFAULT_GUEST_ASSETS_URL = "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide" +MIN_STORAGE_MIB = 30 * 1024 +MAX_STORAGE_MIB = (2**63 - 1) // (1024 * 1024) @dataclass(frozen=True, slots=True) @@ -29,12 +24,14 @@ class Settings: network: IPv4Network = IPv4Network("10.42.0.0/24") gateway: IPv4Address = IPv4Address("10.42.0.1") bridge: str = "uvm0" + architecture: str = field(default_factory=lambda: os.uname().machine) firecracker_version: str = "v1.16.1" default_ram_mib: int = 512 + default_storage_mib: int = MIN_STORAGE_MIB default_vcpu: float = 1 default_ssh_user: str = "root" - kernel_url: str = DEFAULT_KERNEL_URL - rootfs_url: str = DEFAULT_ROOTFS_URL + kernel_url: str = "" + rootfs_url: str = "" firecracker_sha256: str | None = None firecracker_binary_sha256: str | None = None kernel_sha256: str | None = None @@ -48,6 +45,19 @@ class Settings: terminate_timeout_s: float = 5.0 def __post_init__(self) -> None: + architecture = self.architecture.lower() + architecture = {"amd64": "x86_64", "arm64": "aarch64"}.get(architecture, architecture) + if architecture not in ("x86_64", "aarch64"): + raise ConfigurationError(f"unsupported host architecture: {self.architecture}") + object.__setattr__(self, "architecture", architecture) + + # Resolve defaults at construction time, while preserving individual URL overrides. + asset_base = f"{DEFAULT_GUEST_ASSETS_URL}/{architecture}" + if not self.kernel_url: + object.__setattr__(self, "kernel_url", f"{asset_base}/kernels/vmlinux.bin") + if not self.rootfs_url: + object.__setattr__(self, "rootfs_url", f"{asset_base}/rootfs/bionic.rootfs.ext4") + if self.gateway not in self.network: raise ConfigurationError( f"gateway {self.gateway} is outside configured network {self.network}" @@ -124,8 +134,8 @@ class Settings: network=network, gateway=gateway, bridge=os.environ.get("UVM_BRIDGE", "uvm0"), - kernel_url=os.environ.get("UVM_KERNEL_URL", DEFAULT_KERNEL_URL), - rootfs_url=os.environ.get("UVM_ROOTFS_URL", DEFAULT_ROOTFS_URL), + kernel_url=os.environ.get("UVM_KERNEL_URL", ""), + rootfs_url=os.environ.get("UVM_ROOTFS_URL", ""), firecracker_sha256=_optional_environment_value("UVM_FIRECRACKER_SHA256"), firecracker_binary_sha256=_optional_environment_value( "UVM_FIRECRACKER_BINARY_SHA256" diff --git a/uvm/domain.py b/uvm/domain.py index 8d55afc..6906ef9 100644 --- a/uvm/domain.py +++ b/uvm/domain.py @@ -2,16 +2,18 @@ from __future__ import annotations +import math import time import uuid from dataclasses import dataclass, field from ipaddress import IPv4Address from typing import Any +from .config import MAX_STORAGE_MIB, MIN_STORAGE_MIB from .errors import StateError, ValidationError -STATE_SCHEMA_VERSION = 2 +STATE_SCHEMA_VERSION = 3 @dataclass(frozen=True, slots=True) @@ -52,6 +54,40 @@ class VmSpec: password: str = field(default="root", repr=False) name: str | None = None ports: tuple[PortRule, ...] = (PortRule(22),) + storage_mib: int = MIN_STORAGE_MIB + + +@dataclass(frozen=True, slots=True) +class VmResources: + """Complete resource target retained while a stop/resize/start is pending.""" + + cpu: float + ram_mib: int + storage_mib: int + + def __post_init__(self) -> None: + try: + valid_cpu = type(self.cpu) in (int, float) and math.isfinite(self.cpu) and self.cpu > 0 + except OverflowError: + valid_cpu = False + if not valid_cpu: + raise ValidationError("CPU must be a finite value greater than 0") + if type(self.ram_mib) is not int or self.ram_mib < 128: + raise ValidationError("RAM must be an integer of at least 128 MiB") + if type(self.storage_mib) is not int or not MIN_STORAGE_MIB <= self.storage_mib <= MAX_STORAGE_MIB: + raise ValidationError(f"storage must be between {MIN_STORAGE_MIB} and {MAX_STORAGE_MIB} MiB (minimum 30 GiB)") + + def to_dict(self) -> dict[str, Any]: + return {"cpu": self.cpu, "ram_mib": self.ram_mib, "storage_mib": self.storage_mib} + + @classmethod + def from_dict(cls, value: object) -> "VmResources": + if not isinstance(value, dict) or set(value) != {"cpu", "ram_mib", "storage_mib"}: + raise StateError("invalid pending resize resources") + try: + return cls(**value) + except ValidationError as error: + raise StateError(f"invalid pending resize resources: {error}") from error @dataclass(slots=True) @@ -87,12 +123,16 @@ class VmRecord: spawn_pending: bool = False network: str | None = None bridge: str | None = None + storage_mib: int | None = None + pending_resize: VmResources | None = None def to_dict(self) -> dict[str, Any]: data: dict[str, Any] = { "id": self.id, "cpu": self.cpu, "ram_mib": self.ram_mib, + "storage_mib": self.storage_mib, + "pending_resize": self.pending_resize.to_dict() if self.pending_resize is not None else None, "guest_ip": self.guest_ip, "gateway": self.gateway, "tap": self.tap, @@ -155,6 +195,12 @@ class VmRecord: if ports is not None and len({port.port for port in ports}) != len(ports): raise StateError("VM has duplicate TCP port rules") legacy_ready = bool(value.get("disk")) and value.get("status") in {"running", "stopped"} + storage_mib = value.get("storage_mib") + if storage_mib is not None and ( + type(storage_mib) is not int or not MIN_STORAGE_MIB <= storage_mib <= MAX_STORAGE_MIB + ): + raise StateError("VM storage_mib must be a supported integer capacity of at least 30 GiB or null") + pending_resize = value.get("pending_resize") for name in ("disk_ready", "runtime_owned", "tap_owned", "spawn_pending"): if name in value and type(value[name]) is not bool: raise StateError(f"VM {name} must be a boolean") @@ -200,6 +246,8 @@ class VmRecord: spawn_pending=value.get("spawn_pending", value.get("operation") == "spawning"), network=str(value["network"]) if value.get("network") is not None else None, bridge=str(value["bridge"]) if value.get("bridge") is not None else None, + storage_mib=storage_mib, + pending_resize=VmResources.from_dict(pending_resize) if pending_resize is not None else None, ) except (TypeError, ValueError) as error: raise StateError(f"invalid VM record: {error}") from error diff --git a/uvm/firecracker/__pycache__/__init__.cpython-312.pyc b/uvm/firecracker/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31026deff8d36b53f3377f275e9ea0b55783097c GIT binary patch literal 219 zcmX@j%ge<81gujQWVr(A#~=<2FhUuhIe?7m3@Hpz43&(UOjUNCc_pbud5Jj+Zka`? z$wi6D*{MYe$@zI{ndzlPi6xo&c{&P?0iHSviFqjs1x5MEsl~+#i7AN%KyAf(ewvK8 z*yH0<@{{A^S2BDCS@KI)KO;XkRX-)YA|Dn+WJpejg6HJ|E=eCwCH+L4bXNRbd9_!e+l5Pa#I-P`lo zxGi(i%-c6_X5PGc^PBhXACX89LE(OXBhwZ}=nL}U70xB7)xQ971<5FhWJ;zrDoZD6 ziqMS4Bw45BlAP1}l0Ilz&7b9yyd%SDfow1tbZDOz%7&9+p#3thMY0Xa1`5&WD3Sx0 zkQ`LlOpmwtTALBpXpg$hxKy&q86ASr;oEeT7n^3{k;3rAbWV|lBPv!ToRVHwuyAZ* z;-r9;H}Z;UnSwC|tdx*b@W4x>!Wmt?I3OF@l&TA+0pix+S7g38WrtF_ZdfTxHFR?f zzWt-ATu#-`1IeCDS(DH*!}|1BWPPBcbxR(`*uavjms?jAr&f2u_iw?h3QAI7T3Tji zQJRveOW`CVvod{&O|rA-B9-J$qc~?bi$p`RgF}X{D-zL0s!rz2%t8&{)apJU{y<$p zmcu61Wu}PUhSQX(BK-mF*@;3$s))p@)exFSac0a8Xoi&1CJoafJGIoTV&ttjV{>L6 zEM@besOqXEigs9Z%_QcC30Rvo6)kOt>XW!jMAuCVLF7$Q(z3S+J@rI>9rJi#4Tzp z9G4A+#M2YMJUl!K2M^c94w{Np^%nQs{gUgaHe*^Cll9^h zZi2=nx(m1kYD~_puw}F3t&S#A<|(M)a9g^rjJ`K|^YK3q{%P=|$5)0%%0nYdEr(~> z-vugs_?^JzzzW}8=DX)6uD^Qq)n$HHjoVq~JFoT4$tyjHa!+EZYtKFYN!$vv#(g*j z4JOXus?DV}BW2k>tXO%h+w7R3D{&eNL|_|KaUZz@dMc~H9SDkI){yfWp~Is1Mn0vv zJ&xZxyR#!z|Cba^6GgB=GtA=H;Gk;*a=e&S&GA@h+3N+?&Y3fn~n4($zQ1 zzTM~;RjR+rJcv13S3o5-2#Gl7P2O)u&RLx;~_EC&|0_ zps>5n7ln0Q!eq)6bVIF_A-v76e&#Wr6TyJr2B#@2VU<;MP5w$ju-$CsP7&H5^>TT0z~%B@e%2Jbg^&h5Clf4On@ zM+X+@Tk1Qq6gyf9A0^$7-|Sy*+;MBl;h$WJohpS-5&o{5G7S6hfWv=z zDR#CLKKlTktjOg^CEQdAZ++0S{f1WV*;^5I%r}&U{gw9aul;PK;gO#Y1RuswAVOr{ zRpwrspIGLH$iwD*m$nYfPu)q~nqKCgtz+(A=vo~9cy;_5cZRk69v{$!_`rq&+|7cU>Y6iL z02R5+CZP-ju7H6u4L{}@eY?67Kq4+v3v$+$x?Lq_d_`Y|xZH-~^=HV-yrGa}<^#?{ z-Wk86(PJBg;M1D}bntDKu*iYeUnLQQMcyQ*$i5==J|sd#8ft#?QBJ@Aee_4(v*}yZ zH0nbZ35%R!wRu#g!=u*POxGIiQE9p#;jS6}ziPt6dbJm&$iGF)Xomj{e}TGyE;7GD z7pOjB=l^K~Pcj}HD{y%$J@8B+3OSReN>03x$e4y6rwidj2EeOs4UkmL_60y~Gy}%_0Cudr84pykEDJbk$TmxoA_8RcwC&HOrXi-3?I0jgHFLB1ng$V%5*=yI z-7c)Ns{boNH1VZIl+m&7K?V6idiuJC<0NvvAJCwi50shkGi~wo0_65^aCbzpv7>=WC8> ziaiSOkp{q_j^35_fpYu6QhQ?dSf#uF`smfs_s3VdhsxbUclIwnTk0NK>K?s3`jAJF z_LXpVIo!P*?s*Vue&_ki&(BH!3~d7lti*b6?7OjZe%t)W{JuM}JIDSy{^59O@5tit zVs=q1MbE4a&^l}=TNzGsMNUk8$<04 zA@J)u86J<4LFDfZJXWK|fwFzZxeVmmQH!JVKUJ=M-NxFd*Mycxd)4M}3sc}R93iNPei zwnN1E2VLLC$AE(ml8WGq-6$EF=KlL&4yMjY&ht)O+?v$Xb50&*`yeQQzeg~6u<>)G zBG8YIlkpAf|G{uCsiXSKM24vCzV7_b-OTDa{2K7UtC4vSDmYDw`V<}g67_z8VxOVV yXDISH>iZmZe2&__MB5+vcxq^_|N8E$yH~oNDtA5g2m#~Uwr8k@ZxB?@3jYC@dSL4S literal 0 HcmV?d00001 diff --git a/uvm/firecracker/__pycache__/config.cpython-312.pyc b/uvm/firecracker/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbc87be5c2e53924a7f531e536fe9881530ee8d4 GIT binary patch literal 3937 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 str: """Canonical nft JSON, retaining every effective object, rule and verdict. - Ignore only handles, empty set presentation and nft's redundant protocol - dependencies. Rule order is significant; pure conjunctions and sets are not. + Normalize handles, empty set presentation and implied protocol dependencies + without losing address-family constraints. Rule order is significant; + pure conjunctions and sets are not. Unknown expressions/properties remain different rather than being ignored. """ def canonical(value: Any) -> Any: @@ -442,6 +443,18 @@ def _normalize(objects: list[dict[str, Any]]) -> str: return result return value + def payload_protocols(value: Any) -> set[str]: + if not isinstance(value, dict): + return set() + payload = value.get("payload") + if (set(value) == {"payload"} and isinstance(payload, dict) + and set(payload) == {"protocol", "field"} + and isinstance(payload["protocol"], str) and isinstance(payload["field"], str)): + return {payload["protocol"]} + if set(value) == {"&"} and isinstance(value["&"], list) and len(value["&"]) == 2: + return payload_protocols(value["&"][0]) | payload_protocols(value["&"][1]) + return set() + definitions, rules = [], {} for item in objects: kind, raw = next(iter(item.items())) @@ -490,25 +503,24 @@ def _normalize(objects: list[dict[str, Any]]) -> str: left["&"][1] = bitmask(left["&"][1], tcp_flags) match["right"] = bitmask(match["right"], tcp_flags) - # nft's replayable JSON omits dependencies implied by named payloads. - encoded = json.dumps(expressions, sort_keys=True) - has_tcp = '"protocol": "tcp"' in encoded - has_icmp = '"protocol": "icmp"' in encoded - expressions = [expr for expr in expressions if not ( - (has_tcp and expr == _match(_payload("ip", "protocol"), "tcp")) - or (has_icmp and expr == _match(_payload("ip", "protocol"), "icmp")) - )] - encoded = json.dumps(expressions, sort_keys=True) - expressions = [expr for expr in expressions if not ( - ('"protocol": "arp"' in encoded and expr == _match(_payload("ether", "type"), "arp")) - or (('"protocol": "ip"' in encoded or has_icmp) - and expr == _match(_payload("ether", "type"), "ip")) - )] count = len(expressions) if expressions and (not isinstance(expressions[-1], dict) or set(expressions[-1]) != {"match"}): count -= 1 if all(isinstance(expr, dict) and set(expr) == {"match"} for expr in expressions[:count]): - expressions = sorted(expressions[:count], key=lambda expr: json.dumps(expr, sort_keys=True)) + expressions[count:] + matches = expressions[:count] + protocols = set().union(*(payload_protocols(expr["match"].get("left")) for expr in matches)) + # nft elides ether type ip before ip protocol tcp. Materialize its + # IPv4 constraint BEFORE folding the transport dependency: a TCP + # payload alone can also match IPv6 in a bridge table. + for payloads, ethertype in (({"ip", "icmp"}, "ip"), ({"arp"}, "arp")): + dependency = _match(_payload("ether", "type"), ethertype) + if protocols & payloads and dependency not in matches: + matches.append(dependency) + matches = [expr for expr in matches if not ( + ("tcp" in protocols and expr == _match(_payload("ip", "protocol"), "tcp")) + or ("icmp" in protocols and expr == _match(_payload("ip", "protocol"), "icmp")) + )] + expressions = sorted(matches, key=lambda expr: json.dumps(expr, sort_keys=True)) + expressions[count:] obj["expr"] = expressions rules.setdefault(obj.get("chain", ""), []).append(obj) definitions.sort(key=lambda item: json.dumps(item, sort_keys=True)) diff --git a/uvm/images.py b/uvm/images.py index edff748..2e93642 100644 --- a/uvm/images.py +++ b/uvm/images.py @@ -2,14 +2,15 @@ from __future__ import annotations +import os import re -import shutil +import stat import tempfile from dataclasses import dataclass from pathlib import Path -from .config import Settings -from .errors import UvmError +from .config import MAX_STORAGE_MIB, MIN_STORAGE_MIB, Settings +from .errors import UvmError, ValidationError from .integrity import load_manifest, verify_file from .system import CommandRunner @@ -88,23 +89,92 @@ class ImageStore: allow_unverified=self._settings.allow_unverified_downloads) return kernel - def create_vm_disk(self, source: Path, destination: Path) -> Path: + def create_vm_disk( + self, source: Path, destination: Path, *, storage_mib: int = MIN_STORAGE_MIB, + ) -> Path: """Copy the template before Firecracker opens it read-write for a VM.""" + self._validate_storage(storage_mib) if destination.exists(): raise UvmError(f"VM disk already exists: {destination}") destination.parent.mkdir(parents=True, exist_ok=True) try: - shutil.copy2(source, destination) + self._copy_sparse(source, destination) destination.chmod(0o600) - except OSError as error: + self._resize_filesystem(destination, storage_mib) + except BaseException as error: try: destination.unlink() except FileNotFoundError: pass - raise UvmError(f"could not create VM disk {destination}: {error}") from error + if isinstance(error, OSError): + raise UvmError(f"could not create VM disk {destination}: {error}") from error + raise return destination + def resize_vm_disk(self, disk: Path, storage_mib: int) -> None: + """Resize a proven-offline private disk on a copy, then publish atomically.""" + self._validate_storage(storage_mib) + metadata = disk.lstat() + if (not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1 + or disk.resolve() == self._settings.rootfs_image.resolve()): + raise UvmError(f"private VM disk is unsafe to resize: {disk}") + target_bytes = storage_mib * 1024 * 1024 + if metadata.st_size == target_bytes and self._filesystem_size(disk) == target_bytes: + return # A previous resize may have published its disk before saving metadata. + + descriptor, name = tempfile.mkstemp(prefix=".rootfs-resize-", suffix=".ext4", dir=disk.parent) + os.close(descriptor) + temporary = Path(name) + try: + self._copy_sparse(disk, temporary) + temporary.chmod(0o600) + self._resize_filesystem(temporary, storage_mib) + os.replace(temporary, disk) + directory = os.open(disk.parent, os.O_RDONLY) + try: + os.fsync(directory) + finally: + os.close(directory) + except OSError as error: + raise UvmError(f"could not resize VM disk {disk}: {error}") from error + finally: + temporary.unlink(missing_ok=True) + + @staticmethod + def _validate_storage(storage_mib: int) -> None: + if type(storage_mib) is not int or not MIN_STORAGE_MIB <= storage_mib <= MAX_STORAGE_MIB: + raise ValidationError("storage must be a supported whole-MiB capacity of at least 30 GiB") + + def _copy_sparse(self, source: Path, destination: Path) -> None: + self._runner.run(("cp", "--reflink=auto", "--sparse=always", "--", source, destination), capture=True) + + def _filesystem_size(self, disk: Path) -> int: + result = self._runner.run(("env", "LC_ALL=C", "dumpe2fs", "-h", disk), capture=True) + blocks = re.search(r"^Block count:\s+(\d+)\s*$", result.stdout, re.MULTILINE) + block_size = re.search(r"^Block size:\s+(\d+)\s*$", result.stdout, re.MULTILINE) + if blocks is None or block_size is None: + raise UvmError(f"could not determine ext4 filesystem capacity: {disk}") + return int(blocks.group(1)) * int(block_size.group(1)) + + def _resize_filesystem(self, disk: Path, storage_mib: int) -> None: + """Grow the file before ext4; shrink ext4 before truncating the file.""" + checked = self._runner.run(("e2fsck", "-f", "-p", disk), check=False, capture=True) + if checked.returncode not in (0, 1): + detail = checked.stderr.strip() or checked.stdout.strip() + raise UvmError(f"filesystem check failed before resize ({checked.returncode}): {detail}") + target_bytes = storage_mib * 1024 * 1024 + if disk.stat().st_size < target_bytes: + with disk.open("r+b") as image: + image.truncate(target_bytes) + self._runner.run(("resize2fs", disk, f"{storage_mib}M"), capture=True) + if self._filesystem_size(disk) != target_bytes: + raise UvmError("resized ext4 filesystem does not match the requested capacity") + with disk.open("r+b") as image: + image.truncate(target_bytes) + image.flush() + os.fsync(image.fileno()) + def provision_credentials(self, disk: Path, username: str, password: str) -> None: """Set an existing guest account password in an offline ext4 disk.""" diff --git a/uvm/install.py b/uvm/install.py index 3378df6..bcc923b 100644 --- a/uvm/install.py +++ b/uvm/install.py @@ -11,7 +11,7 @@ import urllib.request from collections.abc import Callable from pathlib import Path -from .config import DEFAULT_KERNEL_URL, DEFAULT_ROOTFS_URL, Settings +from .config import Settings from .errors import UvmError from .images import GuestAssets from .integrity import require_checksum, sha256_file, verify_file, write_manifest @@ -42,7 +42,6 @@ class Installer: return self._install_locked(force_assets=force_assets) def _install_locked(self, *, force_assets: bool) -> tuple[Path, GuestAssets]: - self._validate_guest_asset_architecture() self._validate_integrity_policy() if self._settings.allow_unverified_downloads and self._emit is not None: self._emit( @@ -56,16 +55,6 @@ class Installer: self._write_integrity_manifest(firecracker, assets) return firecracker, assets - def _validate_guest_asset_architecture(self) -> None: - architecture = os.uname().machine - default_kernel = self._settings.kernel_url == DEFAULT_KERNEL_URL - default_rootfs = self._settings.rootfs_url == DEFAULT_ROOTFS_URL - if architecture == "aarch64" and (default_kernel or default_rootfs): - raise UvmError( - "default guest assets support x86_64 only; set both UVM_KERNEL_URL " - "and UVM_ROOTFS_URL to compatible aarch64 assets" - ) - def _validate_integrity_policy(self) -> None: require_checksum( self._settings.firecracker_sha256, @@ -112,6 +101,7 @@ class Installer: "iptables", "nftables", "conntrack", + "coreutils", "e2fsprogs", "util-linux", "ca-certificates", @@ -122,9 +112,7 @@ class Installer: self._runner.run(("apt-get", "install", "-y", *packages)) def install_firecracker_binary(self, *, force: bool = False) -> Path: - architecture = os.uname().machine - if architecture not in ("x86_64", "aarch64"): - raise UvmError(f"unsupported host architecture: {architecture}") + architecture = self._settings.architecture firecracker = self._settings.firecracker_binary jailer = self._settings.jailer_binary diff --git a/uvm/lifecycle.py b/uvm/lifecycle.py index 7182f79..d86f272 100644 --- a/uvm/lifecycle.py +++ b/uvm/lifecycle.py @@ -11,8 +11,8 @@ from dataclasses import dataclass, replace from ipaddress import IPv4Network from pathlib import Path -from .config import Settings -from .domain import PortRule, VmRecord, VmSpec, new_vm_id +from .config import MIN_STORAGE_MIB, Settings +from .domain import PortRule, VmRecord, VmResources, VmSpec, new_vm_id from .errors import ConflictError, NotFoundError, UvmError, ValidationError from .firecracker.api import FirecrackerClient from .firecracker.config import build_config, write_config @@ -22,7 +22,7 @@ from .integrity import load_manifest, verify_file from .network import NetworkManager from .state import StateStore from .system import check_kvm, ensure_data_directories, require_root -from .validation import parse_cpu, parse_name, parse_password, parse_ports, parse_username +from .validation import parse_name, parse_password, parse_ports, parse_timeout, parse_username @dataclass(frozen=True, slots=True) @@ -68,9 +68,7 @@ class LifecycleService: def create(self, spec: VmSpec) -> VmRecord: require_root() - parse_cpu(str(spec.cpu)) - if type(spec.ram_mib) is not int or spec.ram_mib < 128: - raise ValidationError("RAM must be an integer of at least 128 MiB") + VmResources(spec.cpu, spec.ram_mib, spec.storage_mib) parse_username(spec.username) parse_password(spec.password) if spec.name is not None: @@ -86,7 +84,7 @@ class LifecycleService: runtime.mkdir(mode=0o700, exist_ok=False) vm.runtime_owned = True self._save(vm) - disk = self._images.create_vm_disk(assets.rootfs, Path(vm.disk)) + disk = self._images.create_vm_disk(assets.rootfs, Path(vm.disk), storage_mib=spec.storage_mib) self._images.provision_credentials(disk, vm.username, vm.password) vm.disk_ready = True self._save(vm) @@ -164,7 +162,7 @@ class LifecycleService: with self._state_store.operation_lock(): return self._start_locked(self._load_for_operation(identifier)) - def _start_locked(self, vm: VmRecord) -> VmRecord: + def _start_locked(self, vm: VmRecord, *, kernel: Path | None = None) -> VmRecord: self._not_deleting(vm) if vm.ports is None: raise ConflictError("legacy VM has no port policy; stop it, use ports set, then start") @@ -172,22 +170,18 @@ class LifecycleService: if observation == "unknown": raise ConflictError(f"cannot start {vm.id}: process identity is unknown; inspect before retrying") if observation == "alive": + if vm.pending_resize is not None: + raise ConflictError(f"resize is pending for {vm.id}; retry resize or stop it before starting") if vm.status != "running" or vm.operation not in {None, "policy"}: raise ConflictError(f"{vm.id} has an interrupted boot/stop; stop it before starting") return self._sync_ports_locked(vm) - if not vm.disk_ready: - raise ConflictError(f"{vm.id} has no completely provisioned private disk") - disk = Path(vm.disk) - if not disk.is_file() or disk.is_symlink() or disk.stat().st_nlink != 1: - raise UvmError(f"private VM disk is missing or unsafe: {disk}") - if (vm.gateway != str(self._settings.gateway) - or vm.network not in {None, str(self._settings.network)} - or vm.bridge not in {None, self._settings.bridge}): - raise ConflictError("configured network differs from the VM's retained network") - self._check_boot() - kernel = self._images.installed_kernel() + self._validate_resume(vm) + if kernel is None: + self._check_boot() + kernel = self._images.installed_kernel() try: self._cleanup_runtime(vm) + self._apply_pending_resize(vm) vm.network, vm.bridge = str(self._settings.network), self._settings.bridge self._boot(vm, kernel) return vm @@ -209,12 +203,13 @@ class LifecycleService: def _stop_locked(self, vm: VmRecord, *, timeout: float | None = None) -> VmRecord: deleting = vm.operation == "delete" or vm.status == "terminating" - vm.status, vm.operation = "stopping", "delete" if deleting else "stop" + pending = "delete" if deleting else "resize" if vm.pending_resize is not None else None + vm.status, vm.operation = "stopping", pending or "stop" self._save(vm) try: self._cleanup_runtime(vm, timeout=timeout) vm.status = "stopped" - vm.operation = "delete" if deleting else None + vm.operation = pending vm.last_error = None self._save(vm) return vm @@ -232,6 +227,69 @@ class LifecycleService: self._stop_locked(vm, timeout=timeout) return self._start_locked(vm) + def resize( + self, identifier: str, *, cpu: float | None = None, ram_mib: int | None = None, + storage_mib: int | None = None, timeout: float | None = None, + ) -> VmRecord: + """Stop, apply resource changes to the retained disk, then boot under one lock.""" + require_root() + if cpu is None and ram_mib is None and storage_mib is None: + raise ValidationError("resize requires at least one of CPU, RAM, or storage") + VmResources(cpu if cpu is not None else 1, ram_mib if ram_mib is not None else 128, + storage_mib if storage_mib is not None else MIN_STORAGE_MIB) + if timeout is not None: + timeout = parse_timeout(str(timeout)) + with self._state_store.operation_lock(): + vm = self._load_for_operation(identifier) + self._not_deleting(vm) + self._validate_resume(vm) + if self._process.observe(vm) == "unknown": + raise ConflictError(f"cannot resize {vm.id}: process identity is unknown") + # Verify boot prerequisites before stopping an otherwise healthy VM. + self._check_boot() + kernel = self._images.installed_kernel() + previous = vm.pending_resize + capacity = previous.storage_mib if previous is not None else vm.storage_mib + if capacity is None: + capacity = max(MIN_STORAGE_MIB, (Path(vm.disk).stat().st_size + 1024 * 1024 - 1) // (1024 * 1024)) + vm.pending_resize = VmResources( + cpu=cpu if cpu is not None else previous.cpu if previous is not None else vm.cpu, + ram_mib=ram_mib if ram_mib is not None else previous.ram_mib if previous is not None else vm.ram_mib, + storage_mib=storage_mib if storage_mib is not None else capacity, + ) + vm.operation = "resize" + self._save(vm) + try: + self._stop_locked(vm, timeout=timeout) + return self._start_locked(vm, kernel=kernel) + except UvmError as error: + raise UvmError(f"failed to resize {vm.id}: {error}") from error + + def _validate_resume(self, vm: VmRecord) -> None: + if vm.ports is None: + raise ConflictError("legacy VM has no port policy; stop it, use ports set, then start") + if not vm.disk_ready: + raise ConflictError(f"{vm.id} has no completely provisioned private disk") + disk = Path(vm.disk) + if not vm.disk or not disk.is_file() or disk.is_symlink() or disk.stat().st_nlink != 1: + raise UvmError(f"private VM disk is missing or unsafe: {disk}") + if (vm.gateway != str(self._settings.gateway) + or vm.network not in {None, str(self._settings.network)} + or vm.bridge not in {None, self._settings.bridge}): + raise ConflictError("configured network differs from the VM's retained network") + + def _apply_pending_resize(self, vm: VmRecord) -> None: + target = vm.pending_resize + if target is None: + return + if self._process.observe(vm) != "gone": + raise ConflictError("cannot resize a disk without confirmed process exit") + if target.storage_mib != vm.storage_mib: + self._images.resize_vm_disk(Path(vm.disk), target.storage_mib) + vm.cpu, vm.ram_mib, vm.storage_mib = target.cpu, target.ram_mib, target.storage_mib + vm.pending_resize = None + self._save(vm) + def destroy(self, identifier: str, *, timeout: float | None = None) -> VmRecord: require_root() with self._state_store.operation_lock(): @@ -472,7 +530,8 @@ class LifecycleService: results.append({"name": name, "ok": True, "message": "available"}) except (UvmError, OSError) as error: results.append({"name": name, "ok": False, "message": str(error)}) - for command in ("ip", "iptables", "nft", "conntrack", "debugfs", "openssl", "ssh-keygen", "ssh"): + for command in ("ip", "iptables", "nft", "conntrack", "debugfs", "e2fsck", "resize2fs", "dumpe2fs", + "cp", "openssl", "ssh-keygen", "ssh"): path = shutil.which(command) results.append({"name": command, "ok": path is not None, "message": path or "not installed"}) return results @@ -529,7 +588,7 @@ class LifecycleService: state.next_mac_index += 1 directory = self._settings.vm_dir(vm_id) vm = VmRecord( - id=vm_id, name=spec.name, cpu=spec.cpu, ram_mib=spec.ram_mib, + id=vm_id, name=spec.name, cpu=spec.cpu, ram_mib=spec.ram_mib, storage_mib=spec.storage_mib, guest_ip=str(guest_ip), gateway=str(self._settings.gateway), network=str(self._settings.network), bridge=self._settings.bridge, tap=self._network.tap_name(vm_id), mac=mac, diff --git a/uvm/routers/vms.py b/uvm/routers/vms.py index f432fc6..f06d666 100644 --- a/uvm/routers/vms.py +++ b/uvm/routers/vms.py @@ -13,16 +13,19 @@ from ..api_models import ( VmCreateRequest, VmLogResponse, VmResponse, + VmResizeRequest, vm_response, ) from ..app import Application from ..domain import PortRule, VmRecord, VmSpec +from ..errors import ValidationError from ..validation import ( parse_cpu, parse_name, parse_password, parse_ports, parse_ram, + parse_storage, parse_timeout, parse_username, ) @@ -71,6 +74,7 @@ def create_vm( VmSpec( cpu=parse_cpu(str(request.cpu)), ram_mib=parse_ram(str(request.ram)), + storage_mib=parse_storage(str(request.storage)), guest_ip=request.guest_ip, username=parse_username(request.username), password=parse_password(request.password.get_secret_value()), @@ -141,6 +145,25 @@ def stop_vm( return vm_response(vm, policy_status=_mutation_policy_status(vm)) +@router.post("/{vm_id}/resize", response_model=VmResponse) +def resize_vm( + vm_id: str, + request: VmResizeRequest, + timeout: float | None = None, + application: Application = Depends(get_authorized_application), +) -> VmResponse: + if request.cpu is None and request.ram is None and request.storage is None: + raise ValidationError("resize requires at least one of CPU, RAM, or storage") + vm = application.lifecycle.resize( + vm_id, + cpu=parse_cpu(str(request.cpu)) if request.cpu is not None else None, + ram_mib=parse_ram(str(request.ram)) if request.ram is not None else None, + storage_mib=parse_storage(str(request.storage)) if request.storage is not None else None, + timeout=parse_timeout(str(timeout)) if timeout is not None else None, + ) + return vm_response(vm, policy_status=_mutation_policy_status(vm)) + + @router.delete("/{vm_id}", response_model=DestroyResponse) def destroy_vm( vm_id: str, diff --git a/uvm/validation.py b/uvm/validation.py index f0bcc46..661e13e 100644 --- a/uvm/validation.py +++ b/uvm/validation.py @@ -5,13 +5,15 @@ from __future__ import annotations import math import re from collections.abc import Sequence -from decimal import Decimal, InvalidOperation +from decimal import Decimal, InvalidOperation, ROUND_CEILING +from .config import MAX_STORAGE_MIB, MIN_STORAGE_MIB from .errors import ValidationError from .domain import PortRule _RAM_PATTERN = re.compile(r"\s*(\d+(?:\.\d+)?)\s*([BKMG]i?B?)?\s*", re.IGNORECASE) +_STORAGE_PATTERN = re.compile(r"\s*(\d+(?:\.\d+)?)\s*([BKMGT]i?B?)?\s*", re.IGNORECASE) _USERNAME_PATTERN = re.compile(r"[a-z_][a-z0-9_-]{0,31}\$?") _MIB_FACTORS = { "b": Decimal(1) / Decimal(1024 * 1024), @@ -62,6 +64,20 @@ def parse_cpu(value: str) -> float: return cpu +def parse_storage(value: str) -> int: + """Parse GiB-default storage, rounding capacity up to a whole MiB.""" + match = _STORAGE_PATTERN.fullmatch(value) + if match is None: + raise ValidationError(f"invalid storage value: {value}; use e.g. 30G, 50G, or 1T") + factors = {**_MIB_FACTORS, **dict.fromkeys(("t", "tb", "ti", "tib"), Decimal(1024 * 1024))} + amount = Decimal(match.group(1)) * factors[(match.group(2) or "g").lower()] + if amount < MIN_STORAGE_MIB: + raise ValidationError("storage must be at least 30 GiB") + if amount > MAX_STORAGE_MIB: + raise ValidationError(f"storage must not exceed {MAX_STORAGE_MIB} MiB") + return int(amount.to_integral_value(rounding=ROUND_CEILING)) + + def parse_username(value: str) -> str: """Validate a conventional Linux account name."""