- Introduced a standalone guest execution agent to facilitate command execution within the guest VM over Firecracker's vsock. - Enhanced FirecrackerClient to support vsock configuration, validating guest CID and UDS path. - Updated configuration to include vsock settings by default. - Refactored FirecrackerProcessManager to manage runtime sockets more effectively. - Implemented guest agent installation and provisioning in the image store. - Added API endpoints for executing commands in the VM and installing the guest agent. - Improved error handling for guest execution failures and agent availability.
98 lines
4.7 KiB
Python
98 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import Mock
|
|
|
|
from tests.test_lifecycle import LifecycleTestCase
|
|
from uvm.errors import ConflictError, GuestAgentUnavailableError, GuestExecutionError, NotFoundError
|
|
from uvm.guest_exec import ExecResult
|
|
|
|
|
|
class GuestExecLifecycleTests(LifecycleTestCase):
|
|
def test_create_provisions_agent_before_the_disk_is_ready_or_the_guest_is_started(self) -> None:
|
|
def provision(disk, vm_id, username):
|
|
self.host.call("images.provision_agent", vm_id)
|
|
vm = self.store.get(vm_id)
|
|
self.assertFalse(vm.disk_ready)
|
|
self.assertEqual(self.process.observe(vm), "gone")
|
|
self.assertEqual(disk, Path(vm.disk))
|
|
self.assertEqual(username, "worker")
|
|
|
|
self.provision_agent.side_effect = provision
|
|
vm = self.create_vm(username="worker")
|
|
self.assert_call_order(vm, "images.provision_agent", "process.start")
|
|
|
|
def test_execution_pins_the_exact_vm_under_lock_and_waits_without_holding_it(self) -> None:
|
|
vm = self.create_vm(name="worker", ports=())
|
|
result = ExecResult(7, "output", "problem", False, False)
|
|
session = Mock()
|
|
|
|
def connect(path, vm_id, username, pid):
|
|
self.assertTrue(self.store.operation_active)
|
|
self.assertEqual((path, vm_id, username, pid), (self.settings.guest_socket_path(vm.id), vm.id, vm.username, vm.pid))
|
|
return session
|
|
|
|
def execute(command, **options):
|
|
self.assertFalse(self.store.operation_active)
|
|
self.assertFalse(self.store.transaction_active)
|
|
self.service.stop(vm.id)
|
|
return result
|
|
|
|
self.service._guest_connector = Mock(side_effect=connect)
|
|
session.execute.side_effect = execute
|
|
self.assertEqual(self.service.execute(vm.id, "exit 7", sudo=True, timeout_seconds=2), result)
|
|
session.execute.assert_called_once_with("exit 7", sudo=True, timeout_seconds=2)
|
|
session.close.assert_called_once()
|
|
|
|
def test_execution_rejects_aliases_stopped_unknown_and_pending_vms_before_connecting(self) -> None:
|
|
vm = self.create_vm(name="worker")
|
|
self.service._guest_connector = Mock()
|
|
for alias in (vm.name, vm.guest_ip):
|
|
with self.assertRaises(NotFoundError):
|
|
self.service.execute(alias, "id")
|
|
for changes in ({"status": "stopped"}, {"operation": "policy"}, {"operation": "delete"}):
|
|
self.persist(vm, **changes)
|
|
with self.assertRaises(ConflictError):
|
|
self.service.execute(vm.id, "id")
|
|
self.persist(vm, status="running", operation=None)
|
|
self.process.unknown.add(vm.id)
|
|
with self.assertRaises(ConflictError):
|
|
self.service.execute(vm.id, "id")
|
|
self.service._guest_connector.assert_not_called()
|
|
|
|
def test_unavailable_or_lost_agent_does_not_retry_or_mutate_vm_state(self) -> None:
|
|
vm = self.create_vm()
|
|
before = self.settings.state_path.read_bytes()
|
|
self.service._guest_connector = Mock(side_effect=GuestAgentUnavailableError("not ready"))
|
|
with self.assertRaises(GuestAgentUnavailableError):
|
|
self.service.execute(vm.id, "id")
|
|
self.service._guest_connector.assert_called_once()
|
|
session = Mock()
|
|
session.execute.side_effect = GuestExecutionError("connection lost", outcome_unknown=True)
|
|
self.service._guest_connector = Mock(return_value=session)
|
|
with self.assertRaises(GuestExecutionError):
|
|
self.service.execute(vm.id, "id")
|
|
session.execute.assert_called_once()
|
|
session.close.assert_called_once()
|
|
self.assertEqual(self.settings.state_path.read_bytes(), before)
|
|
|
|
def test_agent_upgrade_requires_a_stopped_proven_offline_disk_and_retains_identity(self) -> None:
|
|
vm = self.create_vm(username="worker")
|
|
self.provision_agent.reset_mock()
|
|
with self.assertRaises(ConflictError):
|
|
self.service.install_guest_agent(vm.id)
|
|
self.provision_agent.assert_not_called()
|
|
stopped = self.service.stop(vm.id)
|
|
before = stopped.to_dict()
|
|
|
|
def provision(disk, vm_id, username, *, install):
|
|
self.assertTrue(self.store.operation_active)
|
|
self.assertEqual(self.process.observe(self.store.get(vm_id)), "gone")
|
|
self.assertEqual((disk, username, install), (Path(vm.disk), "worker", True))
|
|
|
|
self.provision_agent.side_effect = provision
|
|
self.assertEqual(self.service.install_guest_agent(vm.id).to_dict(), before)
|
|
self.provision_agent.assert_called_once()
|
|
self.assertEqual(self.store.get(vm.id).to_dict(), before)
|
|
self.assertEqual(len(self.process.started), 1)
|