feat(tools): Enhance guest tools installation and management
- Added new API endpoints for tool catalog and installation management. - Updated usage documentation to include new tool installation features. - Implemented asynchronous tool installation with job tracking. - Introduced integration tests for tool installation in disposable VMs. - Enhanced dashboard UI to reflect tool installation status and usage instructions. - Improved error handling and logging for tool installation processes. - Added support for multiple Python versions and Conda environments. - Refactored tool management logic to ensure better state handling and job persistence.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.test_server import FASTAPI_AVAILABLE
|
||||
|
||||
if FASTAPI_AVAILABLE:
|
||||
from fastapi.testclient import TestClient
|
||||
from tests.browser_tool_server import fixture_application
|
||||
from uvm.server import create_api
|
||||
|
||||
|
||||
@unittest.skipUnless(FASTAPI_AVAILABLE, "FastAPI and httpx are required")
|
||||
class ToolFlowTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = self.enterContext(tempfile.TemporaryDirectory())
|
||||
self.application = fixture_application(Path(temporary), delay=0.01)
|
||||
self.enterContext(patch("uvm.tools.require_root"))
|
||||
self.headers = {"X-UVM-Token": "browser-test-token"}
|
||||
|
||||
def wait_for(self, condition):
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
if value := condition():
|
||||
return value
|
||||
time.sleep(0.01)
|
||||
self.fail("background tool worker did not reach the expected state")
|
||||
|
||||
def test_create_and_later_install_use_real_workers_and_durable_job_responses(self):
|
||||
with TestClient(create_api(self.application, host="127.0.0.1")) as client:
|
||||
response = client.post("/vms", headers=self.headers, json={"name": "tools-vm", "tools": [
|
||||
{"id": "git"}, {"id": "python", "version": "3.12"},
|
||||
]})
|
||||
self.assertEqual(response.status_code, 201, response.text)
|
||||
vm_id = response.json()["id"]
|
||||
self.wait_for(lambda: self.application.tools.store.read(vm_id)["jobs"][-1]["status"] == "succeeded")
|
||||
snapshot = client.get(f"/vms/{vm_id}/tools?refresh=true", headers=self.headers)
|
||||
self.assertEqual(snapshot.status_code, 200, snapshot.text)
|
||||
installed = {item["key"] for item in snapshot.json()["inventory"] if item["installed"]}
|
||||
self.assertEqual(installed, {"git", "python-3.12", "curl"})
|
||||
self.assertEqual(snapshot.json()["latest_job"]["source"], "create")
|
||||
later = client.post("/vms/vm-other/tools/install", headers=self.headers,
|
||||
json={"tools": [{"id": "htop"}], "start_if_stopped": True})
|
||||
self.assertEqual(later.status_code, 202, later.text)
|
||||
job_id = later.json()["id"]
|
||||
self.wait_for(lambda: self.application.tools.store.get_job("vm-other", job_id)["status"] == "succeeded")
|
||||
self.assertEqual(self.application.state_store.get("vm-other").status, "running")
|
||||
self.assertEqual(client.get(f"/vms/{vm_id}/tools/jobs/{job_id}", headers=self.headers).status_code, 404)
|
||||
|
||||
def test_queued_jobs_are_dispatched_on_server_start(self):
|
||||
with self.application.state_store.operation_lock():
|
||||
job = self.application.tools.store.enqueue_locked("vm-test", [{"id": "git"}])
|
||||
with TestClient(create_api(self.application, host="127.0.0.1")):
|
||||
self.wait_for(lambda: self.application.tools.store.get_job("vm-test", job["id"])["status"] == "succeeded")
|
||||
self.assertEqual(self.application.lifecycle.attempts[("vm-test", "git")], 1)
|
||||
|
||||
def test_shutdown_wakes_command_waiter_and_keeps_an_interrupted_job(self):
|
||||
self.application.lifecycle.delay = 30
|
||||
with TestClient(create_api(self.application, host="127.0.0.1")) as client:
|
||||
response = client.post("/vms/vm-test/tools/install", headers=self.headers, json={"tools": [{"id": "git"}]})
|
||||
job_id = response.json()["id"]
|
||||
self.wait_for(lambda: self.application.tools.store.get_job("vm-test", job_id)["status"] == "installing"
|
||||
and bool(self.application.tools._sessions))
|
||||
started = time.monotonic()
|
||||
self.assertLess(time.monotonic() - started, 3)
|
||||
job = self.application.tools.store.get_job("vm-test", job_id)
|
||||
self.assertEqual(job["status"], "interrupted")
|
||||
self.assertTrue(job["outcome_unknown"])
|
||||
self.assertTrue(all(not thread.is_alive() for thread in self.application.tools._threads))
|
||||
Reference in New Issue
Block a user