|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | +"""Draft transport for one protected Actions-only maintenance run.""" |
| 5 | + |
| 6 | +import argparse |
| 7 | +import hashlib |
| 8 | +import json |
| 9 | +import os |
| 10 | +import re |
| 11 | +import subprocess |
| 12 | +import sys |
| 13 | +import tarfile |
| 14 | +import tempfile |
| 15 | +from pathlib import Path |
| 16 | + |
| 17 | +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "scripts/ci")) |
| 18 | +from slurm_client import PYTHON, ActionsContext, Connection |
| 19 | + |
| 20 | +BRANCH = "refs/heads/maintenance/owned-scratch-cleanup-1018-1019" |
| 21 | +FILES = {"controller.json", "srun.log", "result.json"} |
| 22 | +SCRATCH_ROOTS = { |
| 23 | + "1018": "/tmp/dynamo-rocm-1018-LYcWorev", |
| 24 | + "1019": "/tmp/dynamo-rocm-1019-hN5nn17x", |
| 25 | +} |
| 26 | + |
| 27 | +WORKER = r""" |
| 28 | +import json, os, re, socket, subprocess, sys |
| 29 | +job = os.environ.get("SLURM_JOB_ID", "") |
| 30 | +if os.getuid() != 20011 or socket.gethostname().split(".")[0] != "smci300x-ccs-aus-e07-40": |
| 31 | + raise SystemExit("Wrong allocation user or host") |
| 32 | +if not re.fullmatch(r"[1-9][0-9]*", job) or job in ("1018", "1019"): |
| 33 | + raise SystemExit("Expected a new maintenance job") |
| 34 | +print(json.dumps({"event":"allocation", "job_id":job, "node":socket.gethostname(), "uid":os.getuid()}), flush=True) |
| 35 | +payload = sys.stdin.read() |
| 36 | +result = subprocess.run(["/usr/bin/sudo", "-n", "/usr/bin/python3", "-I", "-", job], input=payload, text=True, timeout=480, cwd="/") |
| 37 | +raise SystemExit(result.returncode) |
| 38 | +""" |
| 39 | + |
| 40 | +REMOTE = r""" |
| 41 | +import fcntl, hashlib, json, os, re, stat, subprocess, sys, time |
| 42 | +from pathlib import Path |
| 43 | +run_key, sha = sys.argv[1:] |
| 44 | +if os.getuid() != 20011 or not re.fullmatch(r"gh-[1-9][0-9]*-[1-9][0-9]*", run_key) or not re.fullmatch(r"[a-f0-9]{40}", sha): |
| 45 | + raise SystemExit("Invalid maintenance identity") |
| 46 | +os.umask(0o077) |
| 47 | +base = Path.home().resolve() / "dynamo-rocm-ci" / "maintenance" |
| 48 | +base.mkdir(mode=0o700, parents=True, exist_ok=True) |
| 49 | +if base.resolve() != base or base.stat().st_uid != 20011: |
| 50 | + raise SystemExit("Untrusted maintenance directory") |
| 51 | +lock = os.fdopen(os.open(base / "jobs-1018-1019.lock", os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600), "a") |
| 52 | +lock_info = os.fstat(lock.fileno()) |
| 53 | +if not stat.S_ISREG(lock_info.st_mode) or lock_info.st_uid != 20011 or lock_info.st_nlink != 1: |
| 54 | + raise SystemExit("Untrusted maintenance lock") |
| 55 | +fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) |
| 56 | +run = base / run_key |
| 57 | +run.mkdir(mode=0o700) |
| 58 | +data = json.load(sys.stdin) |
| 59 | +controller = {"run_key":run_key,"source_sha":sha,"started_at":time.time(),"payload_sha256":hashlib.sha256(data["payload"].encode()).hexdigest()} |
| 60 | +(run / "controller.json").write_text(json.dumps(controller)+"\n") |
| 61 | +print(json.dumps({"run_dir":str(run),**controller}), flush=True) |
| 62 | +result = {"status":"failed","run_key":run_key,"source_sha":sha} |
| 63 | +try: |
| 64 | + queue = subprocess.run(["squeue","--noheader","--user=20011","--format=%A"], check=True, capture_output=True, text=True, timeout=30) |
| 65 | + rows = queue.stdout.split() |
| 66 | + if any(not value.isdecimal() for value in rows) or {"1018","1019"}.intersection(rows): |
| 67 | + raise RuntimeError("Old jobs are active or queue identity is ambiguous") |
| 68 | + command = ["srun","--partition=compute-1","--nodelist=smci300x-ccs-aus-e07-40","--nodes=1","--ntasks=1","--cpus-per-task=1","--mem=1G","--gres=none","--time=00:10:00","--immediate=60","--kill-on-bad-exit=1","--chdir=/tmp","--job-name=owned-scratch-"+run_key,"/usr/bin/python3","-I","-c",data["worker"]] |
| 69 | + with (run / "srun.log").open("w") as output: |
| 70 | + completed = subprocess.run(command, input=data["payload"], text=True, stdout=output, stderr=subprocess.STDOUT, timeout=720) |
| 71 | + result["returncode"] = completed.returncode |
| 72 | + result["status"] = "passed" if completed.returncode == 0 else "failed" |
| 73 | + completed.check_returncode() |
| 74 | +finally: |
| 75 | + result["finished_at"] = time.time() |
| 76 | + (run / "result.json").write_text(json.dumps(result)+"\n") |
| 77 | +print(json.dumps(result), flush=True) |
| 78 | +""" |
| 79 | + |
| 80 | +COLLECT = r""" |
| 81 | +import os, re, stat, sys, tarfile |
| 82 | +from pathlib import Path |
| 83 | +key = sys.argv[1] |
| 84 | +if os.getuid() != 20011 or not re.fullmatch(r"gh-[1-9][0-9]*-[1-9][0-9]*", key): |
| 85 | + raise SystemExit("Invalid collection identity") |
| 86 | +root = Path.home().resolve() / "dynamo-rocm-ci" / "maintenance" / key |
| 87 | +if root.exists() and (root.resolve() != root or root.stat().st_uid != 20011): |
| 88 | + raise RuntimeError("Untrusted maintenance directory") |
| 89 | +with tarfile.open(fileobj=sys.stdout.buffer, mode="w|") as archive: |
| 90 | + for name in ("controller.json", "srun.log", "result.json"): |
| 91 | + path = root / name |
| 92 | + if not path.exists(): |
| 93 | + continue |
| 94 | + with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK), "rb") as source: |
| 95 | + info = os.fstat(source.fileno()) |
| 96 | + if not stat.S_ISREG(info.st_mode) or info.st_uid != 20011 or info.st_size > 10_000_000: |
| 97 | + raise RuntimeError("Untrusted maintenance evidence") |
| 98 | + entry = tarfile.TarInfo(name) |
| 99 | + entry.size = info.st_size |
| 100 | + entry.mode = 0o600 |
| 101 | + archive.addfile(entry, source) |
| 102 | +""" |
| 103 | + |
| 104 | + |
| 105 | +def verify_success(context, seen): |
| 106 | + if seen != FILES: |
| 107 | + raise RuntimeError("Successful maintenance is missing required evidence") |
| 108 | + controller = json.loads((context.output / "controller.json").read_text()) |
| 109 | + result = json.loads((context.output / "result.json").read_text()) |
| 110 | + for value in (controller, result): |
| 111 | + if ( |
| 112 | + value.get("run_key") != context.run_key |
| 113 | + or value.get("source_sha") != context.sha |
| 114 | + ): |
| 115 | + raise RuntimeError("Maintenance evidence identity mismatch") |
| 116 | + payload = Path(__file__).with_name("owned_scratch_payload.py").read_bytes() |
| 117 | + if controller.get("payload_sha256") != hashlib.sha256(payload).hexdigest(): |
| 118 | + raise RuntimeError("Maintenance payload identity mismatch") |
| 119 | + if result.get("status") != "passed" or result.get("returncode") != 0: |
| 120 | + raise RuntimeError("Maintenance evidence does not report success") |
| 121 | + events = [] |
| 122 | + for line in (context.output / "srun.log").read_text().splitlines(): |
| 123 | + if line.startswith("{"): |
| 124 | + value = json.loads(line) |
| 125 | + if isinstance(value, dict) and "event" in value: |
| 126 | + events.append(value) |
| 127 | + |
| 128 | + def single(name): |
| 129 | + found = [value for value in events if value["event"] == name] |
| 130 | + if len(found) != 1: |
| 131 | + raise RuntimeError(f"Missing or repeated maintenance event: {name}") |
| 132 | + return found[0] |
| 133 | + |
| 134 | + allocation, before, after = ( |
| 135 | + single(name) for name in ("allocation", "before", "after") |
| 136 | + ) |
| 137 | + job = allocation.get("job_id", "") |
| 138 | + if not re.fullmatch(r"[1-9][0-9]*", job) or job in SCRATCH_ROOTS: |
| 139 | + raise RuntimeError("Invalid maintenance allocation evidence") |
| 140 | + if ( |
| 141 | + allocation.get("uid") != 20011 |
| 142 | + or allocation.get("node", "").split(".")[0] != "smci300x-ccs-aus-e07-40" |
| 143 | + ): |
| 144 | + raise RuntimeError("Unexpected maintenance allocation user or node") |
| 145 | + if ( |
| 146 | + before.get("maintenance_job") != job |
| 147 | + or before.get("node") != "smci300x-ccs-aus-e07-40" |
| 148 | + ): |
| 149 | + raise RuntimeError("Root helper allocation evidence mismatch") |
| 150 | + if after.get("status") != "passed" or any( |
| 151 | + value["event"] == "failure" for value in events |
| 152 | + ): |
| 153 | + raise RuntimeError("Root helper evidence does not report success") |
| 154 | + for phase in (before, after): |
| 155 | + targets = phase.get("targets", {}) |
| 156 | + if set(targets) != set(SCRATCH_ROOTS) or any( |
| 157 | + targets[key].get("scratch") != path for key, path in SCRATCH_ROOTS.items() |
| 158 | + ): |
| 159 | + raise RuntimeError("Maintenance target evidence mismatch") |
| 160 | + if any(after["targets"][key].get("exists") is not False for key in SCRATCH_ROOTS): |
| 161 | + raise RuntimeError("Maintenance targets remain after reported success") |
| 162 | + |
| 163 | + |
| 164 | +def main(): |
| 165 | + parser = argparse.ArgumentParser(description=__doc__) |
| 166 | + parser.add_argument("action", choices=("run", "collect")) |
| 167 | + args = parser.parse_args() |
| 168 | + context = ActionsContext.from_environment() |
| 169 | + if ( |
| 170 | + os.environ.get("GITHUB_REPOSITORY") != "SemiAnalysisAI/dynamo" |
| 171 | + or os.environ.get("GITHUB_REF") != BRANCH |
| 172 | + or context.expected_uid != 20011 |
| 173 | + ): |
| 174 | + raise RuntimeError( |
| 175 | + "One-off maintenance is restricted to its reviewed fork branch" |
| 176 | + ) |
| 177 | + context.output.mkdir(parents=True, exist_ok=True) |
| 178 | + connection = Connection(context) |
| 179 | + if args.action == "run": |
| 180 | + payload = Path(__file__).with_name("owned_scratch_payload.py").read_text() |
| 181 | + (context.output / "payload-sha256.txt").write_text( |
| 182 | + hashlib.sha256(payload.encode()).hexdigest() + "\n" |
| 183 | + ) |
| 184 | + with (context.output / "transport.log").open("w") as output: |
| 185 | + connection.call( |
| 186 | + [*PYTHON, "-c", REMOTE, context.run_key, context.sha], |
| 187 | + input=json.dumps({"payload": payload, "worker": WORKER}), |
| 188 | + text=True, |
| 189 | + stdout=output, |
| 190 | + stderr=subprocess.STDOUT, |
| 191 | + timeout=780, |
| 192 | + ) |
| 193 | + else: |
| 194 | + with tempfile.TemporaryFile() as stream: |
| 195 | + connection.call( |
| 196 | + [*PYTHON, "-c", COLLECT, context.run_key], stdout=stream, timeout=60 |
| 197 | + ) |
| 198 | + stream.seek(0) |
| 199 | + with tarfile.open(fileobj=stream, mode="r:") as archive: |
| 200 | + seen = set() |
| 201 | + for member in archive: |
| 202 | + if ( |
| 203 | + member.name not in FILES |
| 204 | + or member.name in seen |
| 205 | + or not member.isfile() |
| 206 | + or member.size > 10_000_000 |
| 207 | + ): |
| 208 | + raise RuntimeError("Unexpected maintenance artifact") |
| 209 | + seen.add(member.name) |
| 210 | + (context.output / member.name).write_bytes( |
| 211 | + archive.extractfile(member).read() |
| 212 | + ) |
| 213 | + outcome = os.environ.get("MAINTENANCE_OUTCOME") |
| 214 | + if outcome not in {"success", "failure", "cancelled", "skipped"}: |
| 215 | + raise RuntimeError("Expected the Actions maintenance step outcome") |
| 216 | + if outcome == "success": |
| 217 | + verify_success(context, seen) |
| 218 | + |
| 219 | + |
| 220 | +if __name__ == "__main__": |
| 221 | + main() |
0 commit comments