cosmos-run is a wrapper around Ansible. You do not need to learn Ansible
to use it — but knowing five ideas will make the framework stop feeling
arbitrary, and this page is those five ideas and nothing else.
If you have not run anything yet, start with
Your First Experiment.
Everything below refers back to it.
Because the hard part of an experiment is not running commands, it is running
them on the right machines, in the right order, and being able to run them
again tomorrow. Ansible already solves that, and solving it again badly is
how orchestration frameworks die.
What COSMOS adds on top is the part Ansible has no opinion about: which nodes
can satisfy your requirements, whether they are free, how to image them, and
how to reach shared equipment like attenuators and positioners.
You have already used all five. Here are their names.
Each entry in your steps: list is a task:
steps:
- name: Who am I?
target: node
run: hostname
Tasks run in order, and a failing task stops the run. That is why a
description reads top-to-bottom like a procedure.
run: is not "shell". It is a module — Ansible's name for a small program
that performs one kind of action and reports what it changed. run: uses the
command module, which executes a program directly.
This is the single most surprising thing for newcomers, so it is worth stating
plainly: run: does not start a shell, so pipes, &&, $(...) and
redirects are not interpreted. Ask for a shell if you want one:
run: bash -lc 'one thing && another | tee /tmp/out'
Modules are also why the framework can tell you what would happen under
--check: a module knows the difference between "I changed this" and "this was
already so".
You wrote target: node, and node was a label you invented in resources:.
The framework resolves that label to a real host and hands Ansible an
inventory — its list of machines and how to reach them.
You never write hosts: in a description. The framework decides which play
runs where, which is why resolution and feasibility are its job rather than
yours.
An idempotent operation converges on a state rather than repeating an action.
"Make sure this package is installed" is idempotent; "run this installer" is
not.
This is why the framework can check whether a node already carries the right
image and skip a fifteen-minute imaging step — and why re-running a description
after a failure usually does the right thing instead of doubling something.
Ansible modules aim to be idempotent. Your run: commands are not, unless
you write them that way.
A role bundles tasks, defaults and files into something with a name. One role:
step can replace twenty run: steps that you would otherwise have to debug:
steps:
- role: cosmos_lab.orchestration.gnuradio_ber
vars:
cosmos_ber_mods: [bpsk, qpsk]
vars: are the role's inputs. Every role's variables and defaults are listed in
the role library, which is
generated from the collection — so it says what the installed version actually
has, not what someone remembered to write down.
Look there before writing anything by hand. The most common avoidable
mistake in a new description is re-implementing a role that already exists.
Genuinely. These are core Ansible concepts you can ignore while writing
descriptions:
| Why not | |
|---|---|
Playbooks and hosts: blocks |
The framework generates the plays. Your description is data that its playbook reads |
Inventory files, groups, group_vars |
Built at run time from the live testbed. A cached host list is a list of guesses |
Handlers and notify |
Not used by the step kinds |
| Jinja templating, filters, lookups | Only if you write a role |
| Vaults | Only if your experiment needs a secret — see the vault guidance, and never put one in a description |
| Strategies, forks, delegation | The framework's business |
To be precise about what happens: your description is not a playbook. It is
read by the framework's own playbook, which runs four plays — resolve the
experiment onto live nodes, bring those nodes to a usable state, execute your
steps, record the run. Your steps become tasks inside the third.
One place: the external: step, the deliberate escape hatch for what the
step kinds do not cover. It takes raw tasks:
steps:
- name: Tune the receive buffer
target: rx
external:
tasks:
- name: Raise rmem_max
ansible.posix.sysctl:
name: net.core.rmem_max
value: "16777216"
or any stock Galaxy role, unmodified:
- external:
role: geerlingguy.docker
vars: {docker_users: [root]}
external: — and what it does notStill guaranteed: feasibility and suitability were checked before the step
ran, the inventory and group membership are the standard resolved ones, and
teardown runs in an always: block whatever happens here.
Not guaranteed: idempotence, --check safety, and useful tagging are the
content author's responsibility. That boundary is deliberate — pretending
third-party code behaves in ways it was never written to would be worse than
saying so.
So --check is not a safe rehearsal for a description containing external:
unless you know the content honours check mode.
If you want the real thing, none of it is COSMOS-specific and the upstream
documentation is good:
The console runs ansible-core 2.16.3 on Python 3.12, so check that anything
you read applies to that version rather than a newer one.
| Page | For |
|---|---|
| Your First Experiment | If you have not run one yet |
| Writing an experiment | The real workflow, idea to description |
| Role library | What you can reuse instead of writing |
| Step kinds | Everything steps: accepts |
| Description language | Every field |
Author(s): COSMOS team · Last verified: 2026-09-10 — concepts checked against the shipped engine (cosmos-orchestration 0.1.0-291): step kinds, the command-module behaviour of run:, the four plays of the generated playbook, and the external: guarantees quoted from the engine source · Tags: orchestration, ansible, getting-started