Implement guest tool installation and management system

- Added `guest_tools.py` for standalone tool installer functionality.
- Introduced `ToolJobStore` in `tool_store.py` to manage per-VM tool job records.
- Created `ToolService` in `tools.py` for background installation workers using the guest execution channel.
- Enhanced `LifecycleService` in `lifecycle.py` to support tool installation queuing and preparation.
- Updated `guest_exec.py` to handle connection shutdown gracefully.
- Added validation and normalization functions for tool specifications.
- Implemented logging and error handling for tool installation processes.
- Ensured compatibility with Ubuntu 22.04 and ARM64/x86_64 architectures.
This commit is contained in:
2026-09-25 23:02:47 +05:30
parent 645bf6321a
commit e1b934e35e
11 changed files with 1345 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
from __future__ import annotations
from unittest.mock import Mock
from tests.test_lifecycle import LifecycleTestCase
from uvm.errors import ConflictError, NotFoundError, UvmError, ValidationError
from uvm.tool_catalog import ToolSelection
class ToolLifecycleTests(LifecycleTestCase):
def test_create_persists_tool_intent_before_boot(self):
def configure(config):
vm_id = next(iter(self.store.load().vms))
jobs = self.service._tool_store._read(vm_id)["jobs"]
self.assertEqual(jobs[0]["selections"], [{"id": "python", "version": "3.12"}])
self.assertEqual(jobs[0]["status"], "queued")
original = self.service._client_factory
def client(socket, timeout):
result = original(socket, timeout)
result.configure_and_start = configure
return result
self.service._client_factory = client
vm = self.create_vm(tools=(ToolSelection("python", "3.12"),))
self.assertEqual(vm.status, "running")
self.assertEqual(self.service._tool_store.read(vm.id)["jobs"][0]["source"], "create")
def test_invalid_tools_fail_before_reserving_a_vm_and_failed_boot_removes_intent(self):
with self.assertRaises(ValidationError):
self.create_vm(tools=({"id": "invalid"},))
self.assertEqual(self.store.load().vms, {})
self.host.failures["client.configure", None] = UvmError("failed boot")
with self.assertRaises(UvmError):
self.create_vm(tools=(ToolSelection("git"),))
self.assertEqual(list(self.settings.vms_dir.iterdir()), [])
def test_stopped_installation_starts_only_when_requested_and_aliases_are_rejected(self):
vm = self.create_vm(name="worker")
self.service.stop(vm.id)
for alias in (vm.name, vm.guest_ip):
with self.assertRaises(NotFoundError):
self.service.queue_tools(alias, [ToolSelection("git")], start_if_stopped=True)
with self.assertRaises(ConflictError):
self.service.queue_tools(vm.id, [ToolSelection("git")])
job = self.service.queue_tools(vm.id, [ToolSelection("git")], start_if_stopped=True)
self.assertEqual(self.store.get(vm.id).status, "stopped")
ready = self.service.prepare_tool_install(vm.id, start_if_stopped=job["start_if_stopped"])
self.assertEqual(ready.status, "running")
self.assertEqual(ready.guest_ip, vm.guest_ip)
def test_a_tool_step_cannot_reconnect_to_a_restarted_vm(self):
vm = self.create_vm()
identity = vm.pid, vm.process_start_time, vm.boot_id
self.service.restart(vm.id)
self.service._guest_connector = Mock()
with self.assertRaisesRegex(ConflictError, "restarted"):
self.service.execute(vm.id, "id", expected_identity=identity)
self.service._guest_connector.assert_not_called()