- 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.
58 lines
2.8 KiB
Python
58 lines
2.8 KiB
Python
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()
|