Skip to main content

Run Ansible modules as Burr state-machine actions in Python.

Project description

ansiburr

Pardon me, are you Ansi-Burr, Sir?

Run Ansible modules as Burr state-machine actions in Python.

A single decorator wraps an Ansible module call as a Burr @action. The module runs through ansible-runner against the target host. Its result projects into Burr's State, and the action's _last_failed, _last_changed, and _last_msg flags become available to downstream transitions. The output is a standard Burr Application that runs, persists, traces, and serves like any other Burr graph.

ansiburr converting a multi-feature Ansible playbook into a Burr FSM and walking it step by step

The GIF above is a single Ansible playbook (set_fact, block, loop, notify/handlers, changed_when) lifted into a Burr Application via ansiburr.from_playbook(...) and walked one action at a time. Every Ansible task is a discrete observable Burr step, and so is every loop iteration, every notify marker, and every handler. No opaque ansible-playbook invocation in the middle of the trace. The playbook driving it is at examples/from_playbook_advanced/playbook.yml; the walker is at examples/from_playbook_walker.py.

What you can build

  • Self-healing service workflows that observe, decide, and remediate one Ansible module at a time, with every step visible in Burr's tracker.
  • SRE agents where an LLM picks one label from a fixed allow-list of remediation actions and the FSM (not the model) enforces termination and retry policy.
  • Cross-platform automation that gathers facts on the target up front and dispatches to the right modules based on the OS family, init system, or package manager.
  • Plan-then-apply pipelines using Ansible's --check and --diff with a deterministic review gate before any change runs.
  • Polling sub-graphs (port readiness, service health, file existence) where every poll attempt is a discrete step in the trace.

Install

uv add ansiburr
# or
pip install ansiburr

ansible-core is pulled in transitively as a runtime requirement. Install additional collections via ansible-galaxy:

ansible-galaxy collection install community.general community.crypto community.docker ansible.posix

Quickstart

Save as my_fsm.py and run with python my_fsm.py. No remote host or extra setup required: ansible.builtin.ping runs against localhost via the ansible-runner already pulled in by pip install ansiburr.

from burr.core import ApplicationBuilder, action
from ansiburr import module_action, initial_sentinels


# `@module_action` turns a function that returns a dict of Ansible module
# args into a Burr `@action`. The module runs through ansible-runner and
# the result projects into State. `writes=["ping"]` projects the module's
# `ping` field; ansiburr also writes ambient `_last_*` sentinels on every
# call (`_last_failed`, `_last_changed`, `_last_msg`, etc.).
@module_action("ansible.builtin.ping", writes=["ping"])
def check(state):
    return {}


# A regular Burr `@action` is a pure-Python step. It reads from State and
# returns the new State. Mixing module actions and plain actions in the
# same graph is the common pattern.
@action(reads=["ping", "_last_failed", "_last_msg"], writes=["report"])
def summarize(state):
    if state["_last_failed"]:
        return state.update(report=f"ping failed: {state['_last_msg']}")
    return state.update(report=f"ansible reachable: ping={state['ping']!r}")


app = (
    ApplicationBuilder()
    .with_actions(check=check, summarize=summarize)
    .with_transitions(("check", "summarize"))
    .with_state(**initial_sentinels(), ping="", report="")
    .with_entrypoint("check")
    .build()
)

_, _, final = app.run(halt_after=["summarize"])
print(final["report"])
# -> ansible reachable: ping='pong'

From there, the moves are:

  • Add host() to point a group of actions at a remote target without repeating the connection dict.
  • Use host.gather_facts() to expand ansible_facts into top-level state keys (ansible_pkg_mgr, ansible_os_family, etc.) and branch transitions on them.
  • Use wait_until() for polling sub-graphs where each attempt is a discrete trace step.
  • Use check_mode=True + diff=True for plan-then-apply patterns with a deterministic review gate.

Working examples of each are in examples/.

From an existing playbook

If you already have an Ansible playbook, from_playbook(...) lifts it into a runnable Burr Application without rewriting the YAML. The full demo lives in examples/from_playbook/.

For a sense of how much of the wild ansiburr ingests: all six of the most-downloaded geerlingguy roles (the de facto Galaxy benchmark) convert directly from their unmodified published source.

Galaxy role Converted graph
geerlingguy.ansible-role-docker 63 actions, 205 transitions
geerlingguy.ansible-role-mysql 78 actions, 265 transitions
geerlingguy.ansible-role-nginx 40 actions, 153 transitions
geerlingguy.ansible-role-postgresql 63 actions, 186 transitions
geerlingguy.ansible-role-redis 13 actions, 27 transitions
geerlingguy.ansible-role-php 101 actions, 312 transitions

Here's the smaller demo playbook from examples/from_playbook/:

# playbook.yml
- name: tool availability check
  hosts: localhost
  gather_facts: no

  tasks:
    - name: check for git
      ansible.builtin.command:
        cmd: git --version
      register: git_check
      ignore_errors: yes
      changed_when: false

    - name: report git availability
      ansible.builtin.debug:
        msg: "git is installed: {{ git_check.stdout }}"
      when: git_check.rc == 0

    - name: check for jq
      ansible.builtin.command:
        cmd: jq --version
      register: jq_check
      ignore_errors: yes
      changed_when: false
# run.py
import ansiburr

app = ansiburr.from_playbook("playbook.yml")
last_action, _, final = app.run(halt_after=["done", "escalate"])

print(f"git: rc={final['git_check']['rc']} {final['git_check'].get('stdout', '').strip()}")
print(f"jq:  rc={final['jq_check']['rc']}  {final['jq_check'].get('stdout', '').strip()}")

Output (when both binaries are present):

git: rc=0 git version 2.50.1 (Apple Git-155)
jq:  rc=0  jq-1.7.1-apple

The converter handles a substantial subset of single-play Ansible: name, when: (including attribute access on registered names), register:, failed_when:, changed_when:, ignore_errors:, become:, gather_facts:, play-level vars:, block: (group-only), include_tasks: and import_tasks: (literal paths), notify: plus handlers:, loop: and with_items: (literal lists), set_fact:, and Jinja2 templates in task arguments. Each task runs as its own play under the hood, so Jinja references to set_fact-written and registered values resolve through Burr state rather than ansible's cross-play context. rescue: / always:, Jinja-templated loop: or include: values, roles:, multi-play files, and the parallelism keywords (serial: / strategy:) raise UnsupportedPlaybookConstruct at conversion time with the offending node named in the message, so a partially-converted FSM never starts. The full supported-vs-rejected list is in REFERENCE.md. From here, the resulting Application can be hand-edited (add transitions, swap actions, wire in policy gates) or the playbook can stay as the source of truth and be re-converted.

CLI

pip install ansiburr ships an ansiburr command for running and inspecting FSMs without writing a wrapper script.

# Run a playbook directly (no manual conversion).
ansiburr run playbook.yml

# Run a Python module that exposes ``app`` (or a ``build_application()`` callable).
ansiburr run examples/localhost_disk_check.py

# Print the FSM structure as mermaid (default), graphviz dot, or plain text.
ansiburr graph examples/from_playbook/playbook.yml --format text
ansiburr graph examples/from_playbook/playbook.yml --format mermaid
ansiburr graph examples/from_playbook/playbook.yml --format dot

ansiburr run halts on done or escalate by default, and accepts --halt-after ACTION (repeatable) to override.

Demo corpus

examples/ contains twelve self-contained FSMs plus three playbook-conversion demos. Most of the FSMs run against a local Docker container set up by examples/service_remediation/setup.sh; the conversion demos and a few others run locally.

Demo What it shows Collections used
localhost_disk_check Linear chain, pure-Python branching on shell output ansible.builtin
service_remediation Retry loop with state counter, ssh plus become, escalate after N attempts ansible.builtin
cert_rotation Linear-with-skip, idempotent multi-step rotation, pure-Python date math community.crypto, ansible.builtin
config_drift Handler equivalent via _last_changed, validate-before-apply (nginx -t), rollback with reload-after-restore ansible.builtin
user_provisioning Iteration via state counter, mid-loop failure preserves partial state ansible.builtin, ansible.posix
sidecar_lifecycle Container lifecycle FSM running on the controller against local Docker community.docker
log_triage Ansible I/O wrapping a Python parser and a Granite-classifier with a deterministic validator gate ansible.builtin
mast_sre_agent MAST-aligned deep multi-module remediation (12 Ansible modules plus 7 Python actions) ansible.builtin, ansible.posix, community.general
coffee_order_ansible Burr's coffee_order topology with every action body swapped for an Ansible module operating on a filesystem queue ansible.builtin
fact_driven_inspect gather_facts() state expansion; transitions branch on ansible_pkg_mgr ansible.builtin
plan_then_apply check+diff plan, deterministic review gate, wait_until polling sub-graph, apply with verify ansible.builtin
from_playbook Small playbook (command + register + when + Jinja debug) lifted via from_playbook(...) ansible.builtin
from_playbook_advanced Multi-feature playbook (set_fact, block, loop, notify + handlers, changed_when) lifted via from_playbook(...); runs locally ansible.builtin
from_playbook_role Multi-file role-style playbook with ansible.builtin.include_tasks dispatch to tasks/setup-<distro>.yml, mirroring the geerlingguy role shape ansible.builtin

Each example runs in seconds. Run an individual demo with uv run python examples/<name>/fsm.py (or run.py for the conversion demos).

The full library API and the Ansible-playbook-idiom mapping live in REFERENCE.md.

Dependencies and licensing

ansiburr is licensed under the Apache License 2.0. See LICENSE for the full text.

ansiburr imports only Apache-2.0 and MIT licensed code (ansible-runner, burr, pyyaml). At runtime it requires ansible-core, which is licensed under GPL-3.0-or-later and is invoked as a separate subprocess by ansible-runner rather than imported directly. Anyone redistributing an installed ansiburr environment should be aware that the bundled ansible-core component carries GPL-3.0+ obligations, and that individual Ansible collections in the user's ansible_collections path may have their own licenses.

The NOTICE file contains the canonical attribution and license summary.

This README is engineering documentation, not legal advice.

Development

git clone https://github.com/msradam/ansiburr
cd ansiburr
uv sync
uv run pytest
uv run ruff check .
uv run mypy src/ansiburr

Most examples require a small Docker container. examples/service_remediation/setup.sh builds the image and generates a per-clone SSH key; examples/service_remediation/start.sh runs the container.

ansiburr was developed with significant AI assistance (Anthropic's Claude). All changes were reviewed and committed by the project owner.

Acknowledgements

  • The Burr team (Apache Software Foundation) for the FSM substrate.
  • The Ansible community for the module ecosystem.
  • IBM Research and UC Berkeley for the MAST failure-mode taxonomy (blog, arXiv:2503.13657).

License

Apache License 2.0. See LICENSE and NOTICE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ansiburr-0.0.25.tar.gz (362.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ansiburr-0.0.25-py3-none-any.whl (55.0 kB view details)

Uploaded Python 3

File details

Details for the file ansiburr-0.0.25.tar.gz.

File metadata

  • Download URL: ansiburr-0.0.25.tar.gz
  • Upload date:
  • Size: 362.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for ansiburr-0.0.25.tar.gz
Algorithm Hash digest
SHA256 65379e5508a9c5d1edc17ca9ab21e568b8aa68f37a36fd2c4339c6ee13e595fc
MD5 22adac9884ed8e5b51c65dc2ba5e6e7f
BLAKE2b-256 08cc07b166d2eae870c5ee251d59b9c7fd66b761b1c5dc2d4fb26b33871b0d50

See more details on using hashes here.

File details

Details for the file ansiburr-0.0.25-py3-none-any.whl.

File metadata

  • Download URL: ansiburr-0.0.25-py3-none-any.whl
  • Upload date:
  • Size: 55.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for ansiburr-0.0.25-py3-none-any.whl
Algorithm Hash digest
SHA256 2f38a334db1ba78a7232d12a3ad4d2eadf5df3f676b9b80856bc182c249d0d70
MD5 6a8b730f3f9c0a3876cfa2d7bb0b7d59
BLAKE2b-256 daede3063fc6667425e889b7837cbea399716477764e3e3f3730d96a826f19fd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page