- 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.
97 lines
4.4 KiB
Python
97 lines
4.4 KiB
Python
"""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])
|