feat: implement resource resizing for VMs with storage management

- Added KnownHostsManager to handle cleanup of SSH known_hosts entries upon VM deletion.
- Introduced disk_size_bytes method in ImageStore to read logical disk capacity.
- Enhanced LifecycleService to manage VM resource resizing, including CPU, RAM, and storage.
- Updated application configuration to support known_hosts_files for SSH management.
- Modified dashboard to include resource resizing options in the UI.
- Improved error handling and validation for resource inputs in the dashboard.
- Removed obsolete .pyc files from the repository.
This commit is contained in:
2026-09-21 20:37:52 +05:30
parent fb80ce7c8a
commit 4a46685a4c
40 changed files with 1048 additions and 63 deletions
+35
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import shutil
import tempfile
import unittest
from collections.abc import Callable, Iterable, Iterator
@@ -80,6 +81,16 @@ class FakeHost:
return [action for action, target in self.events if target == vm_id]
class FakeKnownHosts:
def __init__(self, host: FakeHost) -> None:
self.host = host
def remove(self, vm: VmRecord) -> None:
self.host.call("known_hosts.remove", vm.id)
if Path(vm.socket).parent.exists():
raise AssertionError("known_hosts cleanup must follow VM runtime deletion")
class FakeFirewall:
def __init__(self, host: FakeHost, network: FakeNetwork) -> None:
self.host = host
@@ -282,12 +293,26 @@ class LifecycleTestCase(unittest.TestCase):
self.store = TrackedStateStore(self.settings)
self.host = FakeHost(self.store)
self.images = ImageStore(self.settings)
self.disk_sizes: dict[Path, int] = {}
self.enterContext(patch.object(self.images, "_copy_sparse", side_effect=shutil.copy2))
self.filesystem_resize = self.enterContext(patch.object(
self.images, "_resize_filesystem",
side_effect=lambda disk, capacity: self.disk_sizes.update({disk: capacity * 1024 * 1024}),
))
self.enterContext(patch.object(
self.images, "disk_size_bytes",
side_effect=lambda disk: self.disk_sizes.get(disk, disk.stat().st_size),
))
self.resize_disk = self.enterContext(patch.object(
self.images, "resize_vm_disk", side_effect=self.resize_vm_disk,
))
self.provision = self.enterContext(patch.object(self.images, "provision_credentials"))
self.copy_disk = self.enterContext(patch.object(
self.images, "create_vm_disk", wraps=self.images.create_vm_disk,
))
self.network = FakeNetwork(self.settings, self.host)
self.process = FakeProcess(self.host)
self.known_hosts = FakeKnownHosts(self.host)
self.clients: dict[str, FakeClient] = {}
self.service = LifecycleService(
self.settings,
@@ -295,6 +320,7 @@ class LifecycleTestCase(unittest.TestCase):
self.images,
self.network, # type: ignore[arg-type]
self.process, # type: ignore[arg-type]
known_hosts=self.known_hosts, # type: ignore[arg-type]
client_factory=self.make_client, # type: ignore[arg-type]
)
self.require_root = self.enterContext(patch("uvm.lifecycle.require_root"))
@@ -307,6 +333,13 @@ class LifecycleTestCase(unittest.TestCase):
self.clients[client.vm_id] = client
return client
def resize_vm_disk(self, disk: Path, storage_mib: int) -> None:
vm_id = disk.parent.name
self.host.call("images.resize_disk", vm_id)
if self.process.observe(self.store.get(vm_id)) != "gone":
raise AssertionError("cannot resize a running or ambiguous VM")
self.disk_sizes[disk] = storage_mib * 1024 * 1024
def create_vm(self, **kwargs) -> VmRecord:
return self.service.create(VmSpec(cpu=1, ram_mib=512, **kwargs))
@@ -510,6 +543,7 @@ class LifecycleTests(LifecycleTestCase):
settings = replace(self.settings, **changed)
service = LifecycleService(
settings, self.store, self.images, self.network, self.process,
known_hosts=self.known_hosts,
client_factory=self.make_client,
)
self.host.events.clear()
@@ -542,6 +576,7 @@ class LifecycleTests(LifecycleTestCase):
settings = replace(self.settings, network=IPv4Network(network), gateway=IPv4Address(gateway))
service = LifecycleService(
settings, self.store, self.images, self.network, self.process,
known_hosts=self.known_hosts,
client_factory=self.make_client,
)
with (