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
+34 -3
View File
@@ -30,7 +30,7 @@ class CliTestCase(unittest.TestCase):
def setUp(self) -> None:
self.vm = replace(
make_vm(), name="web", ports=(PortRule(22), PortRule(80, "HTTP")),
status="running", disk_ready=True,
status="running", disk_ready=True, storage_mib=30 * 1024,
)
self.lifecycle = create_autospec(LifecycleService, instance=True, spec_set=True)
self.listed = ListedVm(vm=self.vm, observed_status="running", policy_status="enforced")
@@ -45,8 +45,9 @@ class CliTestCase(unittest.TestCase):
]
self.lifecycle.create.side_effect = lambda spec: replace(
self.vm, cpu=spec.cpu, ram_mib=spec.ram_mib, username=spec.username,
password=spec.password, name=spec.name, ports=spec.ports,
password=spec.password, name=spec.name, ports=spec.ports, storage_mib=spec.storage_mib,
)
self.lifecycle.resize.return_value = self.vm
self.application = SimpleNamespace(
settings=Settings(), lifecycle=self.lifecycle,
state_store=Mock(spec_set=("operation_lock",)),
@@ -186,10 +187,38 @@ class CliParsingTests(CliTestCase):
self.assertEqual(self.invoke([])[0], 2)
class CliResizeCommandTests(CliTestCase):
def test_resize_forwards_resources_and_timeout_without_changing_identity(self) -> None:
self.lifecycle.resize.return_value = replace(self.vm, cpu=2, ram_mib=4096, storage_mib=50 * 1024)
code, stdout, stderr = self.invoke([
"resize", "web", "--cpu", "2", "--ram", "4G", "--storage", "50G", "--timeout", "0.5", "--json",
])
self.assertEqual(code, 0, stderr)
self.lifecycle.resize.assert_called_once_with("web", cpu=2, ram_mib=4096, storage_mib=51200, timeout=0.5)
result = json.loads(stdout)
self.assertEqual(result["storage_mib"], 51200)
self.assertEqual(result["guest_ip"], self.vm.guest_ip)
self.assertEqual(result["username"], self.vm.username)
self.assertNotIn("password", result)
self.assertNotIn(self.vm.password, stdout + stderr)
def test_partial_resize_preserves_omitted_fields(self) -> None:
code, _stdout, stderr = self.invoke(["resize", "web", "--storage", "60G"])
self.assertEqual(code, 0, stderr)
self.lifecycle.resize.assert_called_once_with("web", cpu=None, ram_mib=None, storage_mib=61440, timeout=None)
def test_invalid_resize_requests_are_rejected_before_lifecycle_calls(self) -> None:
for options in ([], ["--storage", "29G"], ["--ram", "127M"], ["--cpu", "nan"],
["--cpu", "0"], ["--cpu", "2", "--timeout", "-1"], ["--username", "new-user"]):
with self.subTest(options=options):
self.assertEqual(self.invoke(["resize", "web", *options])[0], 2)
self.lifecycle.resize.assert_not_called()
class CliCreateCommandTests(CliTestCase):
def test_create_combines_default_ports_and_stdin_credentials(self) -> None:
code, stdout, stderr = self.invoke([
"create", "--name", "web", "--cpu", "0.5", "--ram", "1G",
"create", "--name", "web", "--cpu", "0.5", "--ram", "1G", "--storage", "50G",
"--guest-ip", "10.42.0.8", "--username", "admin", "--password-stdin",
"--ports", "80:http,443:https", "--ports", "8080:tcp", "--json",
], stdin=io.StringIO(" stdin-secret \r\n"))
@@ -199,6 +228,7 @@ class CliCreateCommandTests(CliTestCase):
self.assertEqual(spec.name, "web")
self.assertEqual(spec.cpu, 0.5)
self.assertEqual(spec.ram_mib, 1024)
self.assertEqual(spec.storage_mib, 50 * 1024)
self.assertEqual(spec.guest_ip, IPv4Address("10.42.0.8"))
self.assertEqual(spec.username, "admin")
self.assertEqual(spec.password, " stdin-secret ")
@@ -222,6 +252,7 @@ class CliCreateCommandTests(CliTestCase):
code, stdout, stderr = self.invoke(["create", *options, "--json"])
self.assertEqual(code, 0, stderr)
self.assertEqual(self.lifecycle.create.call_args.args[0].ports, expected)
self.assertEqual(self.lifecycle.create.call_args.args[0].storage_mib, 30 * 1024)
self.assertNotIn("WARNING", stdout)
self.assertIn("default password", stderr)
self.assertEqual(json.loads(stdout)["policy_status"], "enforced")