Fifteen lines of YAML, three commands, and a file brought home from a node you
have never logged into. No Ansible knowledge assumed — this page explains the
parts as they appear.
If you already know what a description is, skip to
Writing an experiment.
An experiment on COSMOS is a file, not a session. You write down what you
need and what you want done; cosmos-run works out which nodes can do it,
prepares them, runs the steps, and brings the results back.
This page builds the smallest such file that does something real, in three
passes that get progressively more committed:
--resolve-only — asks "could this run, and where?". Needs no--check — a rehearsal. Reports what each step would do, withoutYou will hit two errors on the way. Both are deliberate: they are the two
mistakes almost everyone makes first, and the messages are worth reading.
cosmos/v1 description from scratch and understand every line in it--check before committingrun: does — and, importantly, what it does not do| Time | 15 minutes |
| Difficulty | None assumed |
| Nodes | One, any sandbox |
| Radio | None |
| Image | baseline.ndz |
| Last verified | 2026-09-10 on grid (node1-10), run in 39 s |
Nothing else. No local install: cosmos-run lives on the console.
| Role | What | Why |
|---|---|---|
node |
Any COSMOS node | Nothing special — this is the point |
| Image | Notes |
|---|---|
baseline.ndz |
The plain Ubuntu image. Always reference the stable symlink, never a versioned name like ubuntu2404-baseline.ndz — the symlink is repointed when the generation moves, and your description keeps working |
Already on the console: cosmos-run, and the cosmos_lab.orchestration
Ansible collection it drives.
your laptop ──ssh──> console.<sandbox>.cosmos-lab.org
│
│ cosmos-run reads the description,
│ picks a node, prepares it, runs steps
▼
[ node ] ── results copied back ──> ~/cosmos-artifacts/
Log in to any sandbox console and make a place to keep experiments:
ssh <your-uid>@console.grid.cosmos-lab.org
mkdir -p ~/cosmos-experiments
Create ~/cosmos-experiments/hello-cosmos.yml:
apiVersion: cosmos/v1
name: hello-cosmos
description: One node, one command, one file brought home.
resources:
node:
image: baseline.ndz
steps:
- name: Who am I?
target: node
run: hostname
Four blocks, and that is the whole language for now:
apiVersion / name / description — what this is and what to call it.name becomes the prefix of your results directory.resources — what you need. node is a label you choose; you refer totarget:.steps — what to do, in order.run: — a command to execute on the node.Now ask whether it could run. This is free:
cosmos-run ~/cosmos-experiments/hello-cosmos.yml --resolve-only
It will fail, and this is the first thing worth learning:
experiment description 'hello-cosmos' is not valid (1 problem):
resources.node: declares neither 'requires' nor 'pin'
Without one, there is nothing to resolve this resource onto.
Use 'requires: {}' to accept any available node.
Read that again: it names the field, says what is wrong, and gives you the fix.
Validation errors here are meant to be actionable — if one ever is not, that is
a bug worth reporting.
The framework will not guess. Naming a resource says that you need one; it
does not say which, and there are two ways to answer:
| Use it when | |
|---|---|
requires: {} |
Any node that can do the job will do |
pin: [node1-4.grid.cosmos-lab.org] |
The experiment depends on that node — a fixed attenuator, a particular radio, a cable that runs nowhere else |
Add requires: {}:
resources:
node:
requires: {}
image: baseline.ndz
Run it again:
$ cosmos-run ~/cosmos-experiments/hello-cosmos.yml --resolve-only
TASK [Report the resolution] ***************************************************
ok: [localhost] => {
"msg": "hello-cosmos on grid: node=node1-10.grid.cosmos-lab.org "
}
PLAY [Bring the chosen nodes to a usable state] ********************************
skipping: no hosts matched
It picked a node and skipped everything else. Nothing was reserved, powered,
imaged or touched. Do this on each sandbox you are considering, before you book
time on any of them.
cosmos-run ~/cosmos-experiments/hello-cosmos.yml --check
A dry run reports what each step would do without doing it. Destructive
steps — imaging above all — never execute under --check, which is why it
is the right rehearsal for anything that requests an image.
cosmos-run ~/cosmos-experiments/hello-cosmos.yml
About 40 seconds when the node already carries the right image. If it does not,
add roughly 15 minutes for imaging — the framework checks first and only images
when it must.
One command is a poor souvenir. Add a second step that writes a file, and a
collect: block that fetches it:
steps:
- name: Who am I?
target: node
run: hostname
- name: Write something worth bringing home
target: node
run: bash -lc 'mkdir -p /run/cosmos/hello && { hostname; uname -r; date -u; } > /run/cosmos/hello/hello.txt && cat /run/cosmos/hello/hello.txt'
collect: [/run/cosmos/hello]
run: is not a shellThat bash -lc is not decoration, and this is the second thing worth learning.
run: executes a command directly. It does not start a shell. So pipes,
redirects, &&, $(...), { } and globs are not interpreted — they are
passed along as literal arguments. Write this:
run: mkdir -p /run/cosmos/hello && echo hi > /tmp/f # WRONG
and the words after -p become arguments to mkdir:
"cmd": ["mkdir","-p","/run/cosmos/hello","&&","echo","hi",">","/tmp/f"]
"stderr": "mkdir: invalid option -- 'r'"
If you want shell syntax, ask for a shell:
run: bash -lc 'command one && command two | tee /tmp/out' # RIGHT
One inconsistency to know about: a background step
(run: {cmd: ..., background: true}) is run under/bin/sh -c, so shell
syntax works there without the wrapper. Foreground steps do not. Using
bash -lcexplicitly works in both, which is why this page always does.
Run it again, and you are done.
state succeeded
started 2026-09-10T00:03:20Z (0:00:39 elapsed)
finished 2026-09-10T00:04:00Z rc=0
Everything a run produces lands in one directory named after the experiment:
~/cosmos-artifacts/hello-cosmos-20260910T000320Z/
├── console.log the full run log
├── run.json what ran, where, when
├── run.state.json machine-readable outcome
├── description.yml a verbatim copy of YOUR file
└── node1-10.grid.cosmos-lab.org/
└── run/cosmos/hello/hello.txt what collect: fetched
$ cat ~/cosmos-artifacts/hello-cosmos-*/node1-10*/run/cosmos/hello/hello.txt
node1-10.grid.orbit-lab.org
6.8.0-124-generic
Thu Sep 10 00:03:54 UTC 2026
Two details worth noticing:
description.yml is a verbatim copy of what you ran, not a reference toList past runs with cosmos-run --status, and inspect one with
cosmos-run --status <run-id>.
cosmos-run is a wrapper around Ansible, and you have already used four of
its ideas without meeting them by name:
| You wrote | Ansible calls it | Meaning |
|---|---|---|
a steps: entry |
a task | one unit of work |
run: hostname |
a module (command) |
the thing that does the work |
target: node |
inventory | which machine it happens on |
| running it twice safely | idempotence | re-running converges rather than duplicating |
What you do not need to know: playbooks, handlers, hosts: blocks, Jinja
templating, vaults, or how the inventory is built. The description is the
playbook; the framework generates the rest.
That matters most for one habit: prefer a role to a pile of run: steps. A
role is a packaged, tested, reusable procedure, and one role: step can replace
twenty run: steps that you would otherwise have to debug yourself:
steps:
- role: cosmos_lab.orchestration.gnuradio_ber
vars:
cosmos_ber_mods: [bpsk, qpsk]
See the role library for what
already exists — it is worth a look before writing anything by hand.
If you want to learn Ansible properly, the
upstream documentation
is good and none of it is COSMOS-specific.
Nothing to undo — this experiment leaves no shared equipment in a changed
state. Release your reservation when finished.
Experiments that do change shared state (an attenuator, an RF switch) must put
it back in a teardown: block, which runs whether or not the experiment
succeeded. That is covered in
Writing an experiment.
| Symptom | Cause and fix |
|---|---|
declares neither 'requires' nor 'pin' |
A resource that does not say what it needs. Add requires: {} for any node |
mkdir: invalid option -- 'r', or a command that silently ignores its arguments |
run: is not a shell. Wrap in bash -lc '...' |
--resolve-only reports nothing suitable |
No node in that sandbox matches. Try another sandbox; the message says what was missing |
| The run waits a long time before any step | It is imaging the node (~15 min). Rehearse with --check first, which never images |
The hostname shows orbit-lab.org |
Harmless. Some nodes' own hostnames still carry the legacy domain; the testbed's current name is cosmos-lab.org |
Author(s): COSMOS team · Last verified: 2026-09-10 on grid, node1-10, run end to end in 39 s; both documented errors reproduced verbatim · Tested image/release: baseline.ndz / cosmos-orchestration 0.1.0-285 · Tags: orchestration, getting-started, ansible, cosmos-run