Enhance documentation and validation for VM lifecycle and network management; add tests for legacy cleanup and address validation.

This commit is contained in:
2026-09-09 19:21:00 +05:30
parent ee26962902
commit 3d7af4adb8
9 changed files with 366 additions and 35 deletions
+129 -5
View File
@@ -43,7 +43,7 @@ class LifecycleLookupTests(LifecycleTestCase):
before = self.settings.state_path.read_bytes()
for kwargs, message in (
({"name": vm.name}, "VM name already exists"),
({"name": vm.id}, "VM name already exists"),
({"name": vm.id}, "must not impersonate generated VM IDs"),
({"guest_ip": IPv4Address(vm.guest_ip)}, "IP already allocated"),
):
with self.subTest(kwargs=kwargs), self.assertRaisesRegex(UvmError, message):
@@ -53,6 +53,16 @@ class LifecycleLookupTests(LifecycleTestCase):
self.assertEqual(self.copy_disk.call_count, 1)
self.assertEqual(len(self.process.started), 1)
def test_generated_id_names_are_rejected_before_state_or_host_work(self) -> None:
with self.assertRaisesRegex(ValidationError, "must not impersonate generated VM IDs"):
self.create_vm(name="vm-0123456789abcdef0123456789abcdef")
self.assertFalse(self.settings.state_path.exists())
self.assertEqual(self.store.operation_locks, 0)
self.assertEqual(self.host.events, [])
self.check_kvm.assert_not_called()
self.provision.assert_not_called()
self.copy_disk.assert_not_called()
def test_generated_id_retries_id_name_and_tap_collisions(self) -> None:
with patch("uvm.lifecycle.new_vm_id", return_value="vm-first-same1234567"):
first = self.create_vm(name="reserved-name")
@@ -68,8 +78,9 @@ class LifecycleLookupTests(LifecycleTestCase):
self.assertNotEqual(second.guest_ip, first.guest_ip)
self.assertEqual(self.network.taps, {first.tap: first.id, second.tap: second.id})
def test_ambiguous_persisted_name_ip_or_id_never_selects_an_arbitrary_vm(self) -> None:
first = self.create_vm(name="first")
def test_ambiguous_persisted_name_ip_or_legacy_id_never_selects_an_arbitrary_vm(self) -> None:
with patch("uvm.lifecycle.new_vm_id", return_value="vm-legacy"):
first = self.create_vm(name="first")
second = self.create_vm(name="second")
for identifier in (first.guest_ip, first.id, first.name):
with self.subTest(identifier=identifier):
@@ -85,6 +96,34 @@ class LifecycleLookupTests(LifecycleTestCase):
self.assertEqual(self.settings.state_path.read_bytes(), before)
self.assertEqual(self.host.events, [])
def test_exact_generated_id_wins_over_a_malformed_legacy_name(self) -> None:
first = self.create_vm(name="first")
second = self.create_vm(name="second")
self.persist(second, name=first.id)
before = self.settings.state_path.read_bytes()
second_record = self.store.get(second.id).to_dict()
self.host.events.clear()
self.assertEqual(self.service.inspect(first.id).vm.id, first.id)
self.assertEqual(self.service.find_for_ssh(first.id).id, first.id)
self.assertEqual(self.service.log_path(first.id), Path(first.log))
self.assertEqual([vm.id for vm in self.service.resolve_targets([first.id])], [first.id])
self.assertEqual(self.settings.state_path.read_bytes(), before)
self.assertEqual(self.service.stop(first.id).id, first.id)
self.service.destroy(first.id)
self.assertEqual(self.store.get(second.id).to_dict(), second_record)
self.assertEqual(self.host.actions(second.id), [])
before = self.settings.state_path.read_bytes()
self.host.events.clear()
for method in (self.service.inspect, self.service.find_for_ssh,
self.service.log_path, self.service.stop):
with self.assertRaises(NotFoundError):
method(first.id)
with self.assertRaises(NotFoundError):
self.service.resolve_targets([first.id])
self.assertEqual(self.settings.state_path.read_bytes(), before)
self.assertEqual(self.host.events, [])
def test_target_resolution_rejects_invalid_or_missing_targets_without_mutation(self) -> None:
vm = self.create_vm(name="web")
before = self.settings.state_path.read_bytes()
@@ -102,6 +141,36 @@ class LifecycleLookupTests(LifecycleTestCase):
class LifecyclePortTests(LifecycleTestCase):
def test_set_ports_requires_an_exact_registry_id_not_a_name_or_ip(self) -> None:
vm = self.create_vm(name="web")
before = self.settings.state_path.read_bytes()
self.host.events.clear()
for identifier in (vm.name, vm.guest_ip):
with self.subTest(identifier=identifier), self.assertRaises(NotFoundError):
self.service.set_ports(identifier, ())
self.assertEqual(self.settings.state_path.read_bytes(), before)
self.assertEqual(self.host.events, [])
self.assertEqual(self.service.set_ports(vm.id, ()).ports, ())
def test_frozen_legacy_id_cannot_retarget_policy_after_name_reuse(self) -> None:
with patch("uvm.lifecycle.new_vm_id", return_value="vm-legacy"):
original = self.create_vm(name="original")
selected = self.service.inspect(original.name).vm.id
self.service.destroy(original.id)
replacement = self.create_vm(name=original.id, ports=(PortRule(22), PortRule(80)))
self.assertEqual(self.service.inspect(selected).vm.id, replacement.id)
before = self.settings.state_path.read_bytes()
before_tables = self.network.firewall.tables.copy()
self.host.events.clear()
for mode, ports in (("open", (PortRule(443),)), ("close", (PortRule(80),)), ("set", ())):
with self.subTest(mode=mode), self.assertRaises(NotFoundError):
self.service.set_ports(selected, ports, mode=mode)
self.assertEqual(self.settings.state_path.read_bytes(), before)
self.assertEqual(self.network.firewall.tables, before_tables)
self.assertEqual(self.host.events, [])
self.assertIn(replacement.tap, self.network.active_taps)
self.assertEqual(self.process.observe(replacement), "alive")
def test_stopped_port_edits_and_sync_are_metadata_only(self) -> None:
vm = self.create_vm()
stopped = self.service.stop(vm.id)
@@ -195,8 +264,61 @@ class LifecyclePortTests(LifecycleTestCase):
pending = self.store.get(vm.id)
self.assertEqual(pending.ports, (PortRule(22),))
self.assertEqual(pending.operation, "policy")
self.assertIn("link-down failed", pending.last_error)
self.assertTrue(self.network.has_tap(pending))
self.assertIn(vm.tap, self.network.active_taps)
self.assertEqual(self.network.firewall.status(pending), "quarantined")
self.assertIn("firewall.quarantine", self.host.actions(vm.id))
self.assert_call_order(vm, "firewall.apply", "network.deactivate_tap", "firewall.quarantine")
def test_policy_failure_never_quarantines_a_missing_or_foreign_tap(self) -> None:
for owner in (None, "operator-owned"):
with self.subTest(owner=owner):
vm = self.create_vm(ports=(PortRule(22), PortRule(80)))
if owner is None:
del self.network.taps[vm.tap]
self.network.active_taps.discard(vm.tap)
else:
self.network.taps[vm.tap] = owner
with self.assertRaisesRegex(ConflictError, "unowned TAP"):
self.network.has_tap(vm)
before_taps = self.network.taps.copy()
before_active = self.network.active_taps.copy()
before_tables = self.network.firewall.tables.copy()
self.host.events.clear()
with self.assertRaisesRegex(UvmError, "policy pending"):
self.service.set_ports(vm.id, (PortRule(80),), mode="close")
pending = self.store.get(vm.id)
self.assertEqual((pending.ports, pending.operation), ((PortRule(22),), "policy"))
self.assertEqual(pending.pid, vm.pid)
self.assertEqual(self.network.taps, before_taps)
self.assertEqual(self.network.active_taps, before_active)
self.assertEqual(self.network.firewall.tables, before_tables)
self.assertFalse(set(self.host.actions(vm.id)) & {
"firewall.apply", "firewall.quarantine", "firewall.remove",
"firewall.cleanup_connections", "network.delete_vm_tap", "process.terminate",
})
def test_policy_fallback_rechecks_ownership_after_a_tap_is_replaced(self) -> None:
vm = self.create_vm(ports=(PortRule(22), PortRule(80)))
before_tables = self.network.firewall.tables.copy()
apply = self.network.firewall.apply
def replace_tap_and_fail(record, *, quarantine=False):
if not quarantine:
self.network.taps[record.tap] = "operator-owned"
self.host.failures["firewall.apply", record.id] = UvmError("policy rejected")
return apply(record, quarantine=quarantine)
self.host.events.clear()
with patch.object(self.network.firewall, "apply", side_effect=replace_tap_and_fail) as applied:
with self.assertRaisesRegex(UvmError, "policy pending.*unowned TAP"):
self.service.set_ports(vm.id, (PortRule(80),), mode="close")
self.assertEqual(applied.call_count, 1)
self.assertEqual(self.network.taps[vm.tap], "operator-owned")
self.assertIn(vm.tap, self.network.active_taps)
self.assertEqual(self.network.firewall.tables, before_tables)
self.assertNotIn("firewall.quarantine", self.host.actions(vm.id))
self.assertEqual(self.store.get(vm.id).ports, (PortRule(22),))
def test_failed_close_keeps_tap_down_if_quarantine_also_fails(self) -> None:
vm = self.create_vm(ports=(PortRule(22), PortRule(80)))
@@ -348,10 +470,12 @@ class LifecycleBulkTests(LifecycleTestCase):
self.assertEqual(self.store.get(retained.id).status, "stopped")
def test_bulk_exact_ids_do_not_retarget_a_new_vm_named_after_a_deleted_id(self) -> None:
original = self.create_vm(name="original")
with patch("uvm.lifecycle.new_vm_id", return_value="vm-legacy"):
original = self.create_vm(name="original")
selected = tuple(vm.id for vm in self.service.resolve_targets([original.name]))
self.service.destroy(original.id)
replacement = self.create_vm(name=original.id)
self.assertEqual(self.service.inspect(original.id).vm.id, replacement.id)
before = self.store.get(replacement.id).to_dict()
self.host.events.clear()
results = self.service.run_many("delete", selected)