Dev #1

Merged
kstyagi merged 5 commits from dev into main 2026-09-20 03:41:02 +00:00
9 changed files with 86 additions and 31 deletions
Showing only changes of commit ee26962902 - Show all commits
+10 -8
View File
@@ -58,7 +58,8 @@ Packet integration tests must remain inside disposable isolated namespaces.
```text ```text
./ ./
├── DEVELOPER.md # This document ├── DEVELOPER.md # This document
├── README.md # Operator-facing usage guide ├── README.md # Project overview
├── USAGE.md # Operator command/deployment guide
├── pyproject.toml # Package metadata and dependencies ├── pyproject.toml # Package metadata and dependencies
├── uvm.py # Executable source-tree launcher ├── uvm.py # Executable source-tree launcher
├── uvm/ ├── uvm/
@@ -345,12 +346,12 @@ processes, and allocation reuse.
### Start and Restart ### Start and Restart
Start requires managed ports, a proven-gone process (or an already identified To boot a stopped/proven-gone VM, start requires managed ports, a complete safe
running VM), a complete safe private disk, and compatible retained gateway, private disk, and compatible retained gateway, network, and bridge settings.
network, and bridge settings. It verifies the VMM and installed **kernel**; It verifies the VMM and installed **kernel**; the original rootfs template is
the original rootfs template is not needed. It never recopies the rootfs, not needed. It never recopies the rootfs, reprovisions credentials, or
reprovisions credentials, or regenerates guest SSH host keys. Name/IP/MAC, regenerates guest SSH host keys. Name/IP/MAC, ports, and guest disk changes
ports, and guest disk changes survive stop/start; guest RAM does not. survive stop/start; guest RAM does not.
Start on a healthy running VM synchronizes policy without spawning another Start on a healthy running VM synchronizes policy without spawning another
VMM. An ambiguous process or interrupted live boot/stop is not adopted as a VMM. An ambiguous process or interrupted live boot/stop is not adopted as a
@@ -779,6 +780,7 @@ The packet test skipped in this workspace because namespace creation was
denied (`Operation not permitted`). Document that permission limitation, not denied (`Operation not permitted`). Document that permission limitation, not
an assertion that real packets or a real host were validated. Unit tests and an assertion that real packets or a real host were validated. Unit tests and
nft JSON parser checks are not substitutes for kernel packet-path execution. nft JSON parser checks are not substitutes for kernel packet-path execution.
### Isolated Host Validation ### Isolated Host Validation
No current test automatically boots a real microVM. Before a release, validate No current test automatically boots a real microVM. Before a release, validate
@@ -841,7 +843,7 @@ The code is intentionally honest about current limits:
- Firecracker is launched directly, even though the Jailer binary is - Firecracker is launched directly, even though the Jailer binary is
downloaded. downloaded.
- No cgroup CPU, memory, I/O, or process limits are applied. - No cgroup CPU, memory, I/O, or process limits are applied.
- Fractional CPU is represented by rounding vCPUs up and printing an advisory. - Fractional CPU requests round vCPUs up without host quota enforcement.
- Guest health is inferred from VMM process state, not application readiness. - Guest health is inferred from VMM process state, not application readiness.
- No quotas, DHCP, host-port mapping, UDP/IPv6 services, or network tenancy. - No quotas, DHCP, host-port mapping, UDP/IPv6 services, or network tenancy.
- No guest snapshots, extra disks, production image builder, or SSH-key injection. - No guest snapshots, extra disks, production image builder, or SSH-key injection.
+5 -1
View File
@@ -386,9 +386,13 @@ Host and API settings can be overridden through environment variables:
## Project Layout ## Project Layout
Selected files:
```text ```text
./ ./
├── README.md, USAGE.md, DEVELOPER.md ├── README.md
├── USAGE.md
├── DEVELOPER.md
├── pyproject.toml ├── pyproject.toml
├── uvm.py # Executable compatibility launcher ├── uvm.py # Executable compatibility launcher
├── uvm/ # Private application implementation ├── uvm/ # Private application implementation
+17 -12
View File
@@ -97,6 +97,8 @@ SSH to a known guest IP does not require root.
The following is syntax notation, not a shell script. `VM` means an exact VM The following is syntax notation, not a shell script. `VM` means an exact VM
ID, local name, or guest IPv4 address. Flags are command-specific; in ID, local name, or guest IPv4 address. Flags are command-specific; in
particular, create has no `--dry-run`, and start has no `--timeout`. particular, create has no `--dry-run`, and start has no `--timeout`.
Examples elsewhere are separate scenarios; substitute your VM identifiers
and do not run every destructive example as a single script.
```text ```text
uvm install [--force] uvm install [--force]
@@ -235,7 +237,7 @@ literal secret in the command line or history. Do not enable shell tracing:
```bash ```bash
sudo -v sudo -v
read -r -s -p 'Guest password: ' guest_password IFS= read -r -s -p 'Guest password: ' guest_password
printf '\n' printf '\n'
printf '%s\n' "$guest_password" | sudo ./uvm.py create --name app \ printf '%s\n' "$guest_password" | sudo ./uvm.py create --name app \
--cpu 1 --ram 1G --guest-ip 10.42.0.10 \ --cpu 1 --ram 1G --guest-ip 10.42.0.10 \
@@ -346,8 +348,9 @@ adopt a legacy process, or reconcile a VM. Policy status is a separate snapshot:
| `unknown` | Inspection failed or metadata could not establish policy status. | | `unknown` | Inspection failed or metadata could not establish policy status. |
`enforced` is not a service-connectivity test or a lasting guarantee against `enforced` is not a service-connectivity test or a lasting guarantee against
external rule changes. Mutation JSON may report policy `unknown` without a external rule changes. Dry-run target snapshots may show policy `unknown`
fresh observation; use inspect or ports list for a current readback. because a preview does not verify enforcement. Use inspect or ports list for
a current readback.
### Logs and Doctor ### Logs and Doctor
@@ -441,11 +444,12 @@ support, and compatible network settings are still required. Removing the
original rootfs template does not prevent retained-disk start, although doctor original rootfs template does not prevent retained-disk start, although doctor
checks templates and can report it missing. checks templates and can report it missing.
Start on an already running, identified VM synchronizes its managed port Start on a VM recorded as running with a verified process identity synchronizes
policy without rebooting. Restart is stop followed by start under the same its managed port policy without rebooting. Restart is stop followed by start
operation lock. Unknown process identity, an incomplete/unsafe disk, or pending under the same operation lock. Unknown process identity, an incomplete/unsafe
deletion blocks a new boot; inspect the error rather than editing registry disk, or pending deletion blocks a new boot; inspect the error rather than
flags. An interrupted boot with a live VMM must be stopped before start. editing registry flags. An interrupted boot with a live VMM must be stopped
before start.
### Scenario: Permanently Remove a VM ### Scenario: Permanently Remove a VM
@@ -508,10 +512,11 @@ or NAT can change reachability; a private IP is not by itself an exposure
guarantee, and UVM does not integrate with external firewall managers yet. guarantee, and UVM does not integrate with external firewall managers yet.
UDP and IPv6 service policies are not yet supported. Unsolicited inbound UDP UDP and IPv6 service policies are not yet supported. Unsolicited inbound UDP
is blocked, as is IPv6 in both directions on managed TAPs. Guest-initiated IPv4 TCP/UDP (including is blocked, as is IPv6 in both directions on managed TAPs. Guest-initiated IPv4
DNS queries), replies to that egress, ARP, ICMP echo, and required related ICMP TCP/UDP (including DNS queries), replies to that egress, ARP, ICMP echo, and
errors remain allowed. An empty TCP allowlist is not a complete network required related ICMP errors remain allowed. An empty TCP allowlist is not a
disconnect. Other VMs' policies still apply to guest-to-guest egress. complete network disconnect. Other VMs' policies still apply to guest-to-guest
egress.
### List, Open, Close, and Set ### List, Open, Close, and Set
+1 -1
View File
@@ -396,7 +396,7 @@ class NetworkManagerTests(unittest.TestCase):
self.runner.links = {self.vm.tap: {**self.tap_link, "linkinfo": info}} self.runner.links = {self.vm.tap: {**self.tap_link, "linkinfo": info}}
self.runner.calls.clear() self.runner.calls.clear()
self.vm.tap_owned = True self.vm.tap_owned = True
with self.assertRaises(UvmError): with self.assertRaises(ConflictError):
getattr(self.network, operation)(self.vm) getattr(self.network, operation)(self.vm)
self.assertEqual(self.runner.calls, [LINK_INVENTORY]) self.assertEqual(self.runner.calls, [LINK_INVENTORY])
self.assertIs(self.vm.tap_owned, True) self.assertIs(self.vm.tap_owned, True)
+26 -3
View File
@@ -86,7 +86,7 @@ class StateStoreTests(unittest.TestCase):
self.assertIsNone(vm.network) self.assertIsNone(vm.network)
self.assertIsNone(vm.bridge) self.assertIsNone(vm.bridge)
self.assertFalse(vm.disk_ready) self.assertFalse(vm.disk_ready)
self.assertFalse(vm.runtime_owned) self.assertTrue(vm.runtime_owned)
self.assertFalse(vm.tap_owned) self.assertFalse(vm.tap_owned)
self.assertFalse(vm.spawn_pending) self.assertFalse(vm.spawn_pending)
self.assertEqual(vm.updated_at, vm.created_at) self.assertEqual(vm.updated_at, vm.created_at)
@@ -103,6 +103,8 @@ class StateStoreTests(unittest.TestCase):
self.assertEqual(persisted["next_mac_index"], 2) self.assertEqual(persisted["next_mac_index"], 2)
self.assertEqual(persisted["vms"]["vm-test"]["status"], "stopped") self.assertEqual(persisted["vms"]["vm-test"]["status"], "stopped")
self.assertIsNone(persisted["vms"]["vm-test"]["ports"]) self.assertIsNone(persisted["vms"]["vm-test"]["ports"])
self.assertIs(persisted["vms"]["vm-test"]["runtime_owned"], True)
self.assertIs(persisted["vms"]["vm-test"]["disk_ready"], False)
def test_round_trip_preserves_policy_ownership_and_recovery_fields(self) -> None: def test_round_trip_preserves_policy_ownership_and_recovery_fields(self) -> None:
for ports in (None, (), (PortRule(22), PortRule(8080, "http"))): for ports in (None, (), (PortRule(22), PortRule(8080, "http"))):
@@ -258,8 +260,8 @@ class DomainStateTests(unittest.TestCase):
("stopped", "/tmp/rootfs.ext4", None, (True, True, False)), ("stopped", "/tmp/rootfs.ext4", None, (True, True, False)),
("error", "/tmp/rootfs.ext4", 123, (False, True, True)), ("error", "/tmp/rootfs.ext4", 123, (False, True, True)),
("starting", "/tmp/rootfs.ext4", None, (False, True, False)), ("starting", "/tmp/rootfs.ext4", None, (False, True, False)),
("running", "", 123, (False, False, True)), ("running", "", 123, (False, True, True)),
("unknown", "", None, (False, False, False)), ("unknown", "", None, (False, True, False)),
) )
for status, disk, pid, expected in cases: for status, disk, pid, expected in cases:
with self.subTest(status=status, disk=disk, pid=pid): with self.subTest(status=status, disk=disk, pid=pid):
@@ -270,6 +272,27 @@ class DomainStateTests(unittest.TestCase):
self.assertEqual((vm.disk_ready, vm.runtime_owned, vm.tap_owned), expected) self.assertEqual((vm.disk_ready, vm.runtime_owned, vm.tap_owned), expected)
self.assertIsNone(vm.ports) self.assertIsNone(vm.ports)
def test_empty_disk_legacy_runtime_is_owned_but_explicit_false_is_respected(self) -> None:
for disk_fields in ({}, {"disk": ""}):
with self.subTest(disk_fields=disk_fields):
raw = replace(make_vm(), status="stopped").to_dict()
for field in ("disk", "ports", "disk_ready", "runtime_owned"):
raw.pop(field)
raw.update(disk_fields)
legacy = VmRecord.from_dict(raw)
self.assertEqual(legacy.disk, "")
self.assertIs(legacy.runtime_owned, True)
self.assertIs(legacy.disk_ready, False)
self.assertIsNone(legacy.ports)
self.assertEqual(VmRecord.from_dict(legacy.to_dict()), legacy)
new = VmRecord.from_dict({**raw, "ports": [], "runtime_owned": False})
self.assertIs(new.runtime_owned, False)
self.assertIs(new.disk_ready, False)
self.assertEqual(new.ports, ())
self.assertEqual(VmRecord.from_dict(new.to_dict()), new)
def test_missing_spawn_pending_recovers_only_a_legacy_spawning_operation(self) -> None: def test_missing_spawn_pending_recovers_only_a_legacy_spawning_operation(self) -> None:
for operation in (None, "spawning", "configuring", "stopping", "policy"): for operation in (None, "spawning", "configuring", "stopping", "policy"):
with self.subTest(operation=operation): with self.subTest(operation=operation):
+1 -1
View File
@@ -193,7 +193,7 @@ class VmRecord:
name=str(value["name"]) if value.get("name") is not None else None, name=str(value["name"]) if value.get("name") is not None else None,
ports=ports, ports=ports,
disk_ready=value.get("disk_ready", legacy_ready), disk_ready=value.get("disk_ready", legacy_ready),
runtime_owned=value.get("runtime_owned", bool(value.get("disk"))), runtime_owned=value.get("runtime_owned", True),
tap_owned=value.get("tap_owned", bool(value.get("pid"))), tap_owned=value.get("tap_owned", bool(value.get("pid"))),
operation=str(value["operation"]) if value.get("operation") is not None else None, operation=str(value["operation"]) if value.get("operation") is not None else None,
boot_id=str(value["boot_id"]) if value.get("boot_id") is not None else None, boot_id=str(value["boot_id"]) if value.get("boot_id") is not None else None,
+14 -2
View File
@@ -7,6 +7,7 @@ import shutil
import time import time
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from ipaddress import IPv4Network
from pathlib import Path from pathlib import Path
from .config import Settings from .config import Settings
@@ -99,6 +100,9 @@ class LifecycleService:
raise UvmError(message) from error raise UvmError(message) from error
def _check_boot(self) -> None: def _check_boot(self) -> None:
if not any(self._settings.network.subnet_of(IPv4Network(network))
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")):
raise ValidationError("private ports require a network inside an RFC 1918 IPv4 range")
check_kvm() check_kvm()
manifest = load_manifest(self._settings.integrity_manifest_path) manifest = load_manifest(self._settings.integrity_manifest_path)
checksum = self._settings.firecracker_binary_sha256 checksum = self._settings.firecracker_binary_sha256
@@ -258,12 +262,13 @@ class LifecycleService:
self._network.firewall.remove(vm) self._network.firewall.remove(vm)
def set_ports(self, identifier: str, ports: tuple[PortRule, ...], *, mode: str = "set") -> VmRecord: def set_ports(self, identifier: str, ports: tuple[PortRule, ...], *, mode: str = "set") -> VmRecord:
"""Mutate a frozen registry ID; the CLI resolves names before confirmation."""
require_root() require_root()
ports = self._validated_ports(ports) ports = self._validated_ports(ports)
if mode not in {"open", "close", "set"}: if mode not in {"open", "close", "set"}:
raise ValidationError(f"invalid port operation: {mode}") raise ValidationError(f"invalid port operation: {mode}")
with self._state_store.operation_lock(): with self._state_store.operation_lock():
vm = self._load_for_operation(identifier) vm = self._load_for_operation(identifier, exact_id=True)
self._not_deleting(vm) self._not_deleting(vm)
observation = self._process.observe(vm) observation = self._process.observe(vm)
if observation == "unknown": if observation == "unknown":
@@ -321,7 +326,8 @@ class LifecycleService:
containment_errors = [] containment_errors = []
for contain in ( for contain in (
lambda: self._network.deactivate_tap(vm), lambda: self._network.deactivate_tap(vm),
lambda: self._network.firewall.apply(vm, quarantine=True), lambda: (self._network.firewall.apply(vm, quarantine=True)
if self._network.has_tap(vm) else None),
): ):
try: try:
contain() contain()
@@ -457,6 +463,12 @@ class LifecycleService:
def _reserve_vm(self, spec: VmSpec) -> VmRecord: def _reserve_vm(self, spec: VmSpec) -> VmRecord:
with self._state_store.transaction() as state: with self._state_store.transaction() as state:
for existing in state.vms.values():
if (existing.guest_ip == str(self._settings.gateway)
or existing.gateway != str(self._settings.gateway)
or existing.network not in {None, str(self._settings.network)}
or existing.bridge not in {None, self._settings.bridge}):
raise ConflictError("configured topology conflicts with retained VMs; keep their network settings")
if spec.name is not None and any(spec.name in {vm.name, vm.id} for vm in state.vms.values()): if spec.name is not None and any(spec.name in {vm.name, vm.id} for vm in state.vms.values()):
raise ConflictError(f"VM name already exists: {spec.name}") raise ConflictError(f"VM name already exists: {spec.name}")
vm_id = new_vm_id() vm_id = new_vm_id()
+7 -2
View File
@@ -141,7 +141,10 @@ class NetworkManager:
result = self._runner.run(("ip", "-j", "-d", "link", "show"), capture=True) result = self._runner.run(("ip", "-j", "-d", "link", "show"), capture=True)
try: try:
links = json.loads(result.stdout) links = json.loads(result.stdout)
if not isinstance(links, list) or any(not isinstance(link, dict) for link in links): if not isinstance(links, list) or any(
not isinstance(link, dict) or not isinstance(link.get("ifname"), str)
or not link["ifname"] for link in links
):
raise ValueError("invalid interface inventory") raise ValueError("invalid interface inventory")
matches = [link for link in links if link.get("ifname") == name] matches = [link for link in links if link.get("ifname") == name]
if len(matches) > 1: if len(matches) > 1:
@@ -155,7 +158,9 @@ class NetworkManager:
if link is None: if link is None:
return None return None
info = link.get("linkinfo", {}) info = link.get("linkinfo", {})
is_tap = info.get("info_kind") == "tun" and info.get("info_data", {}).get("type") == "tap" is_tap = (isinstance(info, dict) and info.get("info_kind") == "tun"
and isinstance(info.get("info_data"), dict)
and info["info_data"].get("type") == "tap")
legacy = vm.ports is None and vm.tap_owned and not link.get("ifalias") legacy = vm.ports is None and vm.tap_owned and not link.get("ifalias")
if not is_tap or (link.get("ifalias") != f"uvm:{vm.id}" and not legacy): if not is_tap or (link.get("ifalias") != f"uvm:{vm.id}" and not legacy):
raise ConflictError(f"refusing to modify unowned TAP {vm.tap}") raise ConflictError(f"refusing to modify unowned TAP {vm.tap}")
+5 -1
View File
@@ -86,7 +86,7 @@ def parse_password(value: str) -> str:
def parse_name(value: str) -> str: def parse_name(value: str) -> str:
if re.fullmatch(r"[a-z][a-z0-9-]{0,62}", value) is None: if not isinstance(value, str) or re.fullmatch(r"[a-z][a-z0-9-]{0,62}", value) is None:
raise ValidationError("VM name must be 1-63 lowercase letters, digits, or hyphens, starting with a letter") raise ValidationError("VM name must be 1-63 lowercase letters, digits, or hyphens, starting with a letter")
if re.fullmatch(r"vm-[0-9a-f]{32}", value): if re.fullmatch(r"vm-[0-9a-f]{32}", value):
raise ValidationError("VM names must not impersonate generated VM IDs") raise ValidationError("VM names must not impersonate generated VM IDs")
@@ -94,8 +94,12 @@ def parse_name(value: str) -> str:
def parse_ports(values: Sequence[str]) -> tuple[PortRule, ...]: def parse_ports(values: Sequence[str]) -> tuple[PortRule, ...]:
if not isinstance(values, Sequence) or isinstance(values, (str, bytes)):
raise ValidationError("ports must be a sequence of PORT[:TYPE] strings")
ports: dict[int, PortRule] = {} ports: dict[int, PortRule] = {}
for value in values: for value in values:
if not isinstance(value, str):
raise ValidationError("each port specification must be a string")
for item in value.split(","): for item in value.split(","):
match = re.fullmatch(r"\s*(\d+)(?::(TCP|HTTP|HTTPS))?\s*", item, re.IGNORECASE) match = re.fullmatch(r"\s*(\d+)(?::(TCP|HTTP|HTTPS))?\s*", item, re.IGNORECASE)
if match is None: if match is None: