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:
2026-09-25 23:52:21 +05:30
parent a41aafce83
commit 3e17e3e72f
16 changed files with 729 additions and 34 deletions
+96
View File
@@ -0,0 +1,96 @@
"""Opt-in tool installation smoke test against a prepared, disposable microVM."""
from __future__ import annotations
import json
import os
import time
import unittest
import uuid
from urllib.parse import quote
from urllib.request import Request, urlopen
@unittest.skipUnless(os.environ.get("UVM_TOOLS_INTEGRATION") == "1",
"set UVM_TOOLS_INTEGRATION=1 on an isolated disposable VM host")
class GuestToolsIntegrationTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.api_url = os.environ.get("UVM_API_URL", "http://127.0.0.1:8000").rstrip("/")
cls.token = os.environ["UVM_API_TOKEN"]
cls.vm_id = os.environ["UVM_TOOLS_VM_ID"]
cls.path = "/vms/" + quote(cls.vm_id, safe="")
cls.vm = cls.request("GET", cls.path)
if (cls.vm["id"] != cls.vm_id or cls.vm["observed_status"] != "running"
or cls.vm["username"] == "root" or cls.vm["ports"] != []):
raise RuntimeError("provide a disposable running Ubuntu 22.04 VM with a non-root user and no open TCP ports")
@classmethod
def request(cls, method, path, body=None, *, timeout=120):
request = Request(cls.api_url + path, method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"X-UVM-Token": cls.token, "Content-Type": "application/json"})
with urlopen(request, timeout=timeout) as response:
return json.load(response)
def test_install_catalog_and_use_runtimes_as_the_guest_user(self):
catalog = self.request("GET", "/tools")
selections = [{"id": tool["id"], "version": version} for tool in catalog for version in tool["versions"] or [None]]
body = {"tools": selections, "request_id": str(uuid.uuid4())}
job = self.request("POST", self.path + "/tools/install", body)
deadline = time.monotonic() + 3600
while job["status"] in {"queued", "starting", "waiting", "installing"} and time.monotonic() < deadline:
time.sleep(2)
job = self.request("GET", self.path + "/tools/jobs/" + job["id"])
self.assertEqual(job["status"], "succeeded", json.dumps(job, indent=2))
self.assertEqual(self.request("POST", self.path + "/tools/install", body)["id"], job["id"])
inventory = self.request("GET", self.path + "/tools?refresh=true")
self.assertEqual(len(inventory["inventory"]), 12)
self.assertTrue(all(item["installed"] for item in inventory["inventory"]), inventory)
script = r'''set -eu
test "$(id -u)" != 0
git --version
node -e 'if (1 + 1 !== 2) process.exit(1)'
npm --version
wget --version >/dev/null
curl --version >/dev/null
htop --version
php -r 'if (2 + 2 !== 4) exit(1);'
work=$(mktemp -d "$HOME/nest-tools-smoke.XXXXXX")
name=$(basename "$work")
export MAMBA_ROOT_PREFIX="$HOME/micromamba"
cleanup() {
conda env remove -n "$name" -y >/dev/null 2>&1 || true
micromamba env remove -n "$name" -y >/dev/null 2>&1 || true
rm -rf -- "$work"
}
trap cleanup EXIT
printf 'class Hello { public static void main(String[] args) { System.out.println("ok"); } }\n' > "$work/Hello.java"
javac "$work/Hello.java"
test "$(java -cp "$work" Hello)" = ok
for version in 3.11 3.12 3.13; do
"python$version" -m venv "$work/venv-$version"
"$work/venv-$version/bin/python" -c 'import ssl, sqlite3, sys; assert sys.prefix != sys.base_prefix'
"$work/venv-$version/bin/python" -m pip --version
done
conda create -y -n "$name" python=3.13
conda run -n "$name" python -c 'import sys; assert sys.version_info[:2] == (3, 13)'
eval "$(conda shell.posix hook)"
conda activate "$name"
python -c 'import sys; assert sys.version_info[:2] == (3, 13)'
conda deactivate
micromamba create -y -n "$name" -c conda-forge python=3.12
micromamba run -n "$name" python -c 'import sys; assert sys.version_info[:2] == (3, 12)'
eval "$(micromamba shell hook --shell posix)"
micromamba activate "$name"
python -c 'import sys; assert sys.version_info[:2] == (3, 12)'
micromamba deactivate
'''
result = self.request("POST", self.path + "/exec",
{"command": script, "sudo": False, "timeout_seconds": 1200}, timeout=1220)
self.assertFalse(result["timed_out"], result)
self.assertEqual(result["exit_code"], 0, result)
after = self.request("GET", self.path)
for key in ("id", "guest_ip", "mac", "username", "ports", "cpu", "ram_mib", "storage_mib"):
self.assertEqual(after[key], self.vm[key])