Skip to main content

A thin Python wrapper over Ansible's executor: typed AnsibleHost / AnsibleHosts classes that let you run Ansible modules from Python without pytest-ansible or ansible-runner.

Project description

ansible-host

CI License: Apache 2.0 Ruff

A thin Python wrapper over Ansible's executor. Lets you run Ansible modules from Python with structured results, batch execution, and a host-object-first API — without going through pytest-ansible or ansible-runner.

Status: beta (0.1.0). Built from a working internal implementation; API is stable in shape but may shift in details before 1.0.

Why this exists

Ansible has a great executor and a huge module ecosystem, but the existing Python access paths are awkward for in-test or in-tool use:

pytest-ansible ansible-runner ansible-host
Use case Pytest fixtures AWX-style managed jobs In-process programmatic
API surface ~20% of Ansible's runtime Subprocess + event stream Typed Python objects
Forking, custom callbacks Limited Yes (in subprocess) Yes (native)
Per-call overhead Low High (subprocess + JSON parsing) Low (in-process)
Returns Strings Event stream Structured Python dicts

ansible-host sits where the other two don't: in-process, low-overhead, structured-result execution that you can drop into any Python codebase that wants Ansible underneath.

Install

pip install ansible-host

Requires Python 3.10+ and ansible-core>=2.16,<2.22. Like Ansible itself, the library runs on POSIX systems (Linux, macOS, WSL) — it is not supported on native Windows.

Development

The recommended dev workflow uses uv — it manages the virtualenv directly, sidestepping the python3-venv split on Ubuntu and installing dependencies an order of magnitude faster than pip.

# One-time: install uv (https://docs.astral.sh/uv/getting-started/installation/)
curl -LsSf https://astral.sh/uv/install.sh | sh

# In the repo
uv venv                        # creates .venv with the default Python
uv pip install -e ".[dev]"     # editable install + dev extras
uv run pytest                  # run the test suite
uv run ruff check src tests    # lint

To target a specific Python or ansible-core version (matches the CI matrix):

uv venv --python 3.12
uv pip install -e ".[dev]"
uv pip install "ansible-core==2.19.*"
uv run pytest

pip and a manually-managed venv still work — uv is just the convenience.

Testing

The suite has two tiers:

  • Tier-1 (default) — runs modules in-process over Ansible's local connection. No SSH or external host required; this is what uv run pytest and CI's matrix run.
  • Tier-2 (ssh marker) — exercises Ansible's real SSH transport (connection options, remote module exec, become, multi-host fanout) against a throwaway containerized sshd in tests/ssh/. These tests auto-skip unless an SSH target is configured, so the default run stays green without any SSH setup.

Run the SSH tier locally (requires Docker):

# Start a throwaway SSH target with an ephemeral key authorized on it
ssh-keygen -t ed25519 -N '' -f /tmp/ah_ssh_key
export AH_SSH_PUBLIC_KEY="$(cat /tmp/ah_ssh_key.pub)"
docker compose -f tests/ssh/docker-compose.yml up -d --build

# Point the ssh-marked tests at it
AH_SSH_HOST=127.0.0.1 AH_SSH_PORT=2222 AH_SSH_USER=ansible AH_SSH_KEY=/tmp/ah_ssh_key \
    uv run pytest -m ssh

# Tear down
docker compose -f tests/ssh/docker-compose.yml down -v

CI runs Tier-1 across the full Python × ansible-core matrix, plus a dedicated test-ssh job that builds the container and runs the Tier-2 tests.

Quickstart

30-second try — no SSH, no inventory

AnsibleLocalhost runs modules in-process on the current machine via Ansible's local connection plugin. No inventory file, no SSH, no setup.

from ansible_host import AnsibleLocalhost

host = AnsibleLocalhost()
result = host.ping()
assert result["ping"] == "pong"

result = host.command("uname -a")
print(result["stdout"])

Running against a real host

Drop an inventory file alongside your script:

# inventory.ini
[switches]
sw-01 ansible_host=10.0.0.1 ansible_user=admin
from ansible_host import AnsibleHost

host = AnsibleHost(inventory="inventory.ini", pattern="sw-01")
result = host.shell("show version")
print(result["stdout"])

Multi-host fanout

from ansible_host import AnsibleHosts

hosts = AnsibleHosts(inventory="inventory.ini", pattern="switches")
results = hosts.ping()                       # parallelism via forks=
for hostname, r in results.items():
    print(hostname, r["ping"])

# Container API: index, iterate, len
print(len(hosts), hosts.hostnames)
first = hosts[0]                             # -> AnsibleHost
by_name = hosts["sw-01"]                     # -> AnsibleHost
for h in hosts:
    print(h.hostname)

Dynamic dispatch and task directives

Any Ansible module is callable as a method via __getattr__ (host.<module_name>(...)):

host.copy(src="/etc/hosts", dest="/tmp/hosts.bak")
host.command("rm /tmp/maybe-missing", task_directives={"ignore_errors": True})
host.shell("echo $TOKEN", task_directives={"no_log": True})

Common task_directives: ignore_errors, no_log, when, failed_when, changed_when, become.

Batch mode — queue tasks, run them in a single play

with host:
    host.shell("uptime")
    host.shell("df -h")
    host.shell("free -m")
results = host.results

Building a queue across functions

with host: is lexically scoped. If you need to assemble a batch across multiple functions, use the explicit form — same machinery, no scope limit:

host.load_module("ansible.builtin.command", args=["uptime"])
host.load_module("ansible.builtin.command", args=["df -h"])
results = host.run_loaded_modules()

Result shapes

single task batch (with block)
single host dict list[dict]
multi host {hostname: dict} {hostname: list[dict]}

Reading host variables

Inspect inventory variables resolved by Ansible — host vars, group vars, and runtime extra vars — without running a module:

host = AnsibleHost(inventory="inventory.ini", pattern="sw-01")

# Variables defined directly on the host (raw — Jinja2 templates not rendered):
host.get_host_var("ansible_user")          # -> "admin"
host.host_vars                             # -> dict of host-scoped vars

# Fully-resolved view a host can see (host + group + extra vars), with
# Jinja2 templates rendered:
host.get_visible_var("some_group_var")     # group var visible to the host
host.get_visible_var("missing", "default") # default when absent
host.visible_vars                          # -> dict of all resolved vars

# Runtime overrides (highest precedence):
host.update_extra_vars({"feature_flag": True})
host.extra_vars                            # -> current extra vars

AnsibleHosts exposes the host-keyed form: hosts.get_host_var("sw-01", "ansible_user") and hosts.get_visible_var("sw-01", "some_var").

Failures

A failing module raises AnsibleModuleFailed:

from ansible_host import AnsibleModuleFailed

try:
    host.command("false")
except AnsibleModuleFailed as e:
    print("module failed:", e)

# Or suppress and inspect the result:
result = host.command("false", task_directives={"ignore_errors": True})
assert result["failed"] is True

More examples

See the test suite for runnable examples:

Compatibility

This library uses Ansible's internal Python API (TaskQueueManager, InventoryManager, VariableManager, Play, DataLoader). Those APIs are not officially stable across ansible-core releases — expect occasional updates when ansible-core introduces breaking internal changes. The current support range is declared in pyproject.toml and in the matrix CI.

Concurrency

ansible-host is designed for sequential use at the instance level. For parallelism within a single call, use Ansible's native forking via the forks= option. Running multiple AnsibleHost / AnsibleHosts instances concurrently in different threads will race on ansible.context's process-global state.

Reference implementation

A real-world use of this pattern lives in sonic-mgmt's tbng branchansible-host is the cleaned-up, packaged version of that code.

License

Apache License 2.0. See LICENSE.

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

ansible_host-0.1.0.tar.gz (33.1 kB view details)

Uploaded Source

Built Distribution

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

ansible_host-0.1.0-py3-none-any.whl (23.3 kB view details)

Uploaded Python 3

File details

Details for the file ansible_host-0.1.0.tar.gz.

File metadata

  • Download URL: ansible_host-0.1.0.tar.gz
  • Upload date:
  • Size: 33.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ansible_host-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1b032444ff07587886cb9679093fff1bb848144414acd6ac53b3994970b83bcf
MD5 d68bfd7b8a03d9b55a2d1969773a5d2b
BLAKE2b-256 1b29eb7076b1ae24eef2d19a676758dc132d9b1ee2ec410dfe85d67944d4c6e5

See more details on using hashes here.

Provenance

The following attestation bundles were made for ansible_host-0.1.0.tar.gz:

Publisher: publish.yml on wangxin/ansible-host

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ansible_host-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: ansible_host-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 23.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ansible_host-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ead887c5668aaf4bd4afaa4ff0b573921713b4a124da8f99b50ac94e20a6b08d
MD5 11a78e12058264e75b9be7b6ebeca0a0
BLAKE2b-256 63866f9214d8221bca80e1fb075acc5e69ca33d5e342f15be67b11f4433e3c9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for ansible_host-0.1.0-py3-none-any.whl:

Publisher: publish.yml on wangxin/ansible-host

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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