From af76763c16797d943be320c63e14be02ae2bc03f Mon Sep 17 00:00:00 2001 From: sjat Date: Thu, 18 Jun 2026 12:19:58 +0200 Subject: [PATCH] feat(integration-vm): golden image fetch + SHA512 verification --- scripts/integration-vm.py | 42 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/scripts/integration-vm.py b/scripts/integration-vm.py index 09dfc40..2979676 100644 --- a/scripts/integration-vm.py +++ b/scripts/integration-vm.py @@ -44,9 +44,6 @@ DEFAULT_VCPUS = 2 MIN_FREE_MIB = 4096 VALID_TIERS = ("internal", "le-staging", "le-prod-wildcard") -DISPATCH = {} # temporary; real dispatch added in a later task - - def vm_name(host, suffix=None): suffix = suffix or uuid.uuid4().hex[:8] return f"{NAME_PREFIX}{host}-{suffix}" @@ -108,6 +105,45 @@ def render_run_hosts(name, ip, ansible_user, groups): return "\n".join(lines) + "\n" +def sh(cmd, check=True, capture=False, **kw): + """Run a command (list form). Logs the command to stderr.""" + print("+ " + " ".join(str(c) for c in cmd), file=sys.stderr) + return subprocess.run(cmd, check=check, + capture_output=capture, text=True, **kw) + + +def _expected_sha(sha_text, filename): + for line in sha_text.splitlines(): + parts = line.split() + if len(parts) == 2 and parts[1].lstrip("*") == filename: + return parts[0] + return None + + +def ensure_image(): + CACHE_DIR.mkdir(parents=True, exist_ok=True) + img = CACHE_DIR / IMAGE_NAME + if img.exists(): + return img + print(f"Downloading {IMAGE_URL} ...", file=sys.stderr) + tmp = img.with_suffix(".part") + urllib.request.urlretrieve(IMAGE_URL, tmp) + sha_text = urllib.request.urlopen(SHA_URL).read().decode() + want = _expected_sha(sha_text, IMAGE_NAME) + if not want: + tmp.unlink(missing_ok=True) + raise SystemExit(f"checksum for {IMAGE_NAME} not found at {SHA_URL}") + h = hashlib.sha512() + with open(tmp, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 20), b""): + h.update(chunk) + if h.hexdigest() != want: + tmp.unlink(missing_ok=True) + raise SystemExit("golden image SHA512 mismatch — refusing to use it") + tmp.rename(img) + return img + + def main(argv=None): p = argparse.ArgumentParser(prog="integration-vm", description=__doc__) sub = p.add_subparsers(dest="cmd", required=True)