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,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])
|
||||
Reference in New Issue
Block a user