Scenario format¶
A scenario has two files with the same name:
| File | Repository | Contents |
|---|---|---|
src/greenray/data/scenarios/<id>.yaml |
problem | Problem: briefing, objectives, setup (fault injection), checks (grading), hints |
src/greenray/data/solutions/scenarios/<id>.yaml |
answer | Answer: reference solution, explanation, edge cases |
The loader refuses a problem file that contains solution, explanation or edge_cases, so the lab UI can always keep the answer hidden until it is asked for.
Prefix ids with the domain: ins-, adm-, wkl-, tsh-. The file name must equal the id.
Problem file¶
id: tsh-example
title: Short, symptom-style title
domain: Troubleshooting and Optimization # one of the 4 blueprint domains, spelled exactly
difficulty: medium # easy | medium | hard
time_limit_min: 15
tags: [slurm, xid] # `slurm`, `kubernetes` or `bcm` makes it eligible for `greenray mock`
briefing: |
What the user or ticket says. Describe symptoms, not the cause.
objectives:
- What "done" means, in words (shown to the learner)
setup: # faults applied to a fresh healthy cluster, in order
- set: nodes.dgx-03.gpus.5.xid # dotted path; list indexes allowed
value: 79
- append: nodes.dgx-03.dmesg
value: "[ 8812.402113] NVRM: Xid (PCI:0000:5d:00): 79, pid=0, GPU has fallen off the bus."
- merge: slurm.nodes.dgx-03
value: {state: DRAIN, reason: "healthcheck: GPU missing"}
- file: /etc/slurm/gres.conf
host: dgx-03
content: "Name=gpu File=/dev/nvidia[0-3]\n"
- python: | # escape hatch for complex setups; `state` is in scope
for g in state.nodes['dgx-03'].gpus: g.temp_c = 80
checks: # Python expressions; all must be true to pass
- desc: dgx-03 is back in service
expr: slurm.nodes['dgx-03'].state == 'IDLE'
- desc: you checked the kernel log # evidence: ran() does not know order
expr: ran(r'^dmesg', 'dgx-03')
- desc: you read the log before rebooting # ran_before() does
kind: evidence
expr: ran_before(r'^dmesg', r'^reboot', 'dgx-03')
- desc: nothing was rebooted with a job still on it
kind: safety
expr: not audit('reboot_with_jobs')
hints: [gentle, more specific, nearly the answer]
references:
- Public doc titles to read next
Answer file¶
solution: # replayed by the test-suite; must make every check pass
- ssh dgx-03
- dmesg | grep -i xid
- | # multi-line step: first line is the command, the rest is heredoc input
cat > /etc/slurm/gres.conf <<EOF
Name=gpu File=/dev/nvidia[0-7]
EOF
explanation: |
Why this is the fix, and what each piece of evidence meant.
edge_cases:
- Exam traps, variations, and what changes on other hardware.
Variants¶
A scenario may carry a variants: list so that a repeat is a different problem: another node, GPU, job id or user.
variants:
- {node: dgx-02, gpu: 3, bus: 5D, job: 1500, user: alice}
- {node: dgx-04, gpu: 6, bus: BA, job: 2317, user: dave}
Every variant must have the same keys. {node}, {gpu} and the other names are substituted in the title, briefing, objectives, setup, checks and hints, and in the solution file's solution, explanation and edge cases. Only names that exist in the variant are replaced, so Python dict literals and regex quantifiers such as \D{0,4} in checks are left alone. A plain scalar in YAML may not start with {, so quote values that begin with a placeholder.
greenray lab <id> picks a variant at random, --variant N chooses one, and retry in incident mode moves to the next. greenray validate and the test-suite replay every variant.
How scenarios are presented¶
greenray lab <id>(guided) shows the title, briefing and objectives.greenray incidentshows only the briefing, so write the briefing as an incident report: symptoms, impact, and every constraint the learner must respect (e.g. "don't kill bob's job"). It must never name the root cause.- Objectives and check descriptions may name the cause. In incident mode they stay hidden until solved, or until the learner chooses
objectives. - Hints should go from a pointer to an area, to a diagnostic command, to (only in the last hint) something close to the fix. The last hint is shown only after confirmation. A hint may name the tool and the setting, but never quotes a fix command from the solution verbatim;
greenray validateand the test-suite check this by replaying the solution and comparing every state-changing step with every hint.
Writing checks¶
These names are available in checks expressions: state, nodes, slurm, k8s, bcm, ngc, dcgm, flags. Helpers: ran(regex, host=None), file(host, path), pods(namespace=None, prefix=''), release(name), jobs(**fields) and re.
- Grade the end state, so that any valid solution passes, and accept every legitimate way to reach it.
- Use
ran()to require evidence gathering. It does not know the order of commands;ran_before(earlier, later, host=None)does: both regexes must have matched, the first match ofearlierbefore the first oflater. audit(kind)lists destructive operations the simulator recorded while they cost users work:reboot_with_jobs,power_off_with_jobs,node_down_with_jobs(a node set DOWN, not drained, with jobs on it),cancel_user_job(scancel of another user's job),delete_running_podandruntime_restart_with_containers. A check such asnot audit('reboot_with_jobs')rewards draining first.- A check may declare
kind: safetyorkind: evidence; a check whose expression usesran(is evidence by default, everything else is an outcome. Results show the tag, so learners see what each criterion rewards. Every kind counts the same towards passing. - A check that raises an exception counts as failed. The error text is shown only by
greenray validateor withGREENRAY_DEBUG=1. - Scenario files are trusted code (
evalfor checks,execforpython:setup). Never load them from untrusted sources. - State objects are dicts with attribute access. A key that clashes with a dict method (
values,items,keys,get,update,pop) must be read withobj['values'], notobj.values. For Helm releases, userelease('x').computed, which holds the chart defaults merged with user values. - The state layout is documented in
src/greenray/core/state.py.
Checklist for a new scenario¶
- Add the id to
docs/coverage.mdunder its blueprint topic. uv run greenray validateshould showokfor it.uv run pytestshould pass.