Skip to content

Commit 4c6dbcf

Browse files
chore(ci): reclaim scratch from cancelled development jobs
Signed-off-by: functionstackx <47992694+functionstackx@users.noreply.github.com>
1 parent 47d86dc commit 4c6dbcf

3 files changed

Lines changed: 479 additions & 68 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

Comments
 (0)