"""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])