Ansible, from one Python script
Your whole playbook repo. One script.
Instead of editing a directory full of small YAML files, you edit one script, and either dump the playbook repo or run it outright.
pip install -e .imix init site.pypython site.py run
# site.py
from instamix import file
file("site.yml", """
- hosts: localhost
become: true
tasks:
- debug:
msg: "Helloworld!"
""")$ python site.py run
PLAY [localhost] ***************************************************
TASK [debug] *******************************************************
ok: [localhost] => {
"msg": "Helloworld!"
}
What it gives you
The parts of a playbook repository, as calls in one file
file() is the core. Everything else is a shorthand that
calls it, or says what the script is made of.
Declare files, not directories
Indent a triple-quoted block to match your code; parent directories are implicit; a dict or a list is dumped as YAML.
Roles, collections, inventory
playbook(), role(), collection(), inventory() and group_vars() are where the multi-file editing goes away.
A script that describes itself
Groups, variables with a shape, tags and stages. A run refuses to start when a needed variable is not set.
One script, several deployments
A declaration inside with profile("prod") only materializes when that profile is active.
A controller, built to order
The ansible-core, Python and pip packages a script requires, resolved by uv into a lock with hashes. Or run it in a container.
Checks over the whole tree
The tree is in memory at once, so a missing handler, role or template is found before ansible-lint even starts.
Ansible inside the strings
A language server for VS Code and Neovim: completion, hover and ansible-lint problems on the line that declared the file.
Tests next to the roles
Molecule scenarios, ansible-test targets and role test playbooks, declared where the role is.
Absorb what you have
imix import turns an existing playbook tree into a script, byte for byte, and leaves the secrets out.
Describe it once
Stages that run alone, variables that are held to their shape
A script says what it has and what each part is for. The inventory,
the group_vars and site.yml are written from that, and a
script that imports another gets all of it.
- Every group, variable, tag and stage has a description, and it is required.
runrefuses a tag the script does not describe: ansible itself would run nothing and say nothing.- Stitch scripts together with a plain
import.
from glom import Or
from instamix import Template, file, group, stage, var
group("web", "Hosts that serve the site", hosts=["web1", "web2"])
var("site_name", "The name the site answers to", group="web")
var("site_port", "The port nginx listens on",
Or(int, Template), default=8080, group="web")
stage("base", "Prepare the hosts", file("base.yml", """
- name: Prepare the hosts
hosts: web
...
"""))$ python site.py describe
Stages (run one with --tags):
base Prepare the hosts
app Install the app [when: app_enabled | bool]
Variables of web:
site_name: str (needed)
The name the site answers to
site_port: Or(int, Template) = 8080
The port nginx listens on
$ python site.py run --tags app --limit web1
See it work
In the editor, and in a run
imix lsp runs next to your Python server, and each string
is the file it declares: completed, linted and documented as Ansible.
Then the same script runs.
examples/webserver2.py.Beyond one machine
From a script to a run that someone allowed
The same script runs from your shell, or from a catalogue, through a gate. Nothing in between writes a command: a job is the script, the stage, the hosts and the values.
The inventory hub
Hosts, groups and variables per team, served as an ansible dynamic inventory, with forms in the shape the script describes.
An MCP server that only proposes
One tool per recipe. Its arguments are the recipe's variables, as JSON Schema. None of the tools runs anything.
As a pod
A kustomize base, the hub and clockwork in containers of their own, and a component that gives each job its own pod.
The screening proxy
Only what a list lets through
imix proxy serve stands between ansible-galaxy, uv and
pip and the real Galaxy and PyPI. It caches what they download and
serves only what a list of rules allows. The first line that matches
decides.
- Refused versions are left out of version lists; a refused download is a 403, and it is logged.
- A script can pin its downloads to the list named after its own SHA-256.
- A blacklist from OSV keeps malicious PyPI packages out.
# web servers
allow collection nginxinc.nginx_core <0.8
allow collection community.*
deny any *evil*
allow pip requests >=2.30
default allow roleFor other programs
Every command speaks JSON, and there is a contract
With --jsonl, stdout carries one event per line. An
OpenAPI document describes each command, each event and each
declaration call, and the tests keep it from drifting.
The reference on this site is read from that same document.
$ python site.py run --jsonl -C
{"event":"exec","cmd":["ansible-playbook","-i","inventory","--check","site.yml"],"cwd":"/tmp/instamix-..."}
{"event":"output","stream":"stdout","line":"PLAY [localhost] ****..."}
{"event":"exited","cmd":["ansible-playbook","-i","inventory","--check","site.yml"],"status":0}
Start with one file
Write a starter script, or absorb the playbooks you have.
$ imix init site.py
$ imix import ./old-playbooks -o site.py