TESTINFRA(1)

πŸ“¦ version 10.2.2, in arch rolling

okay so bestie, you're looking at the TESTINFRA(1) manual page, and it's literally the glow-up guide for testing your servers. Buckle up πŸ’…

🌸 NAME 🌸

  • testinfra - testinfra Documentation

Latest documentation: https://testinfra.readthedocs.io/en/latest

✨ ABOUT ✨

With Testinfra you can write unit tests in Python to test the actual state of your servers configured by management tools like Salt (https://saltstack.com/), Ansible (https://www.ansible.com/), Puppet (https://puppetlabs.com/), Chef (https://www.chef.io/) and so on. 🫢

Testinfra aims to be a Serverspec (https://serverspec.org/) equivalent in python and is written as a plugin to the powerful Pytest (https://pytest.org/) test engine. πŸ”₯

πŸŽ€ LICENSE πŸŽ€

Apache License 2.0 (https://github.com/pytest-dev/pytest-testinfra/blob/main/LICENSE)

The logo is licensed under the Creative Commons NoDerivatives 4.0 License (https://creativecommons.org/licenses/by-nd/4.0/). If you have some other use in mind, contact us. πŸ’–

πŸ¦‹ QUICK START πŸ¦‹

Install testinfra using pip:

$ pip install pytest-testinfra

# or install the devel version
$ pip install 'git+https://github.com/pytest-dev/pytest-testinfra@main#egg=pytest-testinfra'

Write your first tests file to test_myinfra.py:

def test_passwd_file(host):
    passwd = host.file("/etc/passwd")
    assert passwd.contains("root")
    assert passwd.user == "root"
    assert passwd.group == "root"
    assert passwd.mode == 0o644

def test_nginx_is_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed
    assert nginx.version.startswith("1.2")

def test_nginx_running_and_enabled(host):
    nginx = host.service("nginx")
    assert nginx.is_running
    assert nginx.is_enabled

And run it:

$ pytest -v test_myinfra.py

====================== test session starts ======================
platform linux -- Python 2.7.3 -- py-1.4.26 -- pytest-2.6.4
plugins: testinfra
collected 3 items

test_myinfra.py::test_passwd_file[local] PASSED
test_myinfra.py::test_nginx_is_installed[local] PASSED
test_myinfra.py::test_nginx_running_and_enabled[local] PASSED

=================== 3 passed in 0.66 seconds ====================

πŸ“š DOCUMENTATION πŸ“š

πŸ—“οΈ Changelog πŸ—“οΈ

10.2.2

  • [FIX] Include files required to run tests in sdist

10.2.1

  • [FIX] Fix releasing to pypi

10.2.0

  • [NEW] Query all usernames and group names
  • [FIX] Prevent Paramiko deadlock when test sends more than 2MB to stdout
  • [FIX] Follow changes in ansible shell module
  • [FIX] Add 4 to the expected exit code when running "systemctl is-active"
  • [FIX] Fix KeyError in MountPoint.__repr__() if mount does not exist
  • [DOC] Use pytest command instead of py.test
  • [DOC] Extend backend documentation with a general host spec section
  • [MISC] Also run lint for py 3.12 and 3.13
  • [MISC] Switch packaging to use hatchling
  • [MISC] Drop unused extra "args" argument to run_winrm()
  • [MISC] Use ruff format instead of black/isort
  • [MISC] Use ruff instead of flake8
  • [MISC] Use f-string instead of str.format()
  • [MISC] Use builtin dict, list and tuple for typing
  • [MISC] Use python 3.11 during tests
  • [MISC] Fix salt tests
  • [MISC] Fix tests failing due to expiration date passed
  • [MISC] Remove crypt lib from testing

10.1.1

  • [FIX] Add check for the existence of ssh control path directory
  • [FIX] Handle is_installed for rpm package when rpm database is corrupted
  • [FIX] Fix service.exists

10.1.0

  • [NEW] Add Interface.link property
  • [FIX] Make file properties follow symlinks
  • [FIX] Require pytest>=6 and use future annotations for pytest<7 compatibility

10.0.0

  • [FIX] Ansible: Fix for missing group names in get_variables()
  • [FIX] testinfra/modules/blockdevice: Don't fail on stderr
  • [DOC] Extend and show the documentation of CommandResult
  • [FIX] Extend list of valid suffixes for systemd units
  • [DOC] Add missing Environment doc section
  • [MISC] Define types for plugin.py
  • [FIX] Missing RHEL distribution in package module
  • [NEW] Add brew support in package module
  • [NEW] Add Service.exists
  • [MISC] Make CommandResult a dataclass

9.0.0

  • [BREAKING] pytest-testinfra now require python >= 3.9
  • [BREAKING] Drop deprecated module PipPackage
  • [NEW] Add support for the SSH ControlPath connection sharing option (#713)
  • [FIX] Retry SSH on ConnectionResetError (#708)
  • [FIX] List openSUSE Leap and Tumbleweed explicitly as rpm based distributions
  • [FIX] Make group name mandatory in group module

8.1.0

  • [NEW] Add Windows support for File and Service modules
  • [NEW] Add File.is_executable property

8.0.0

  • [NEW] Add Group.members attribute
  • [NEW] Add File.inode attribute
  • [NEW] Add Interface.routes() method
  • [NEW] Add Docker.is_restarting attribute
  • [FIX] Fix possible error in Interface.default()
  • [FIX] Fix busybox detection in Process module
  • [FIX] Fix possible KeyError in SysInfo module
  • [BREAKING] Drop support for python 3.7

7.0.1

  • [FIX] Fix command -v compatibility with dash shell

7.0.0

  • [NEW] Improved ssh config support in Paramiko backend
  • [NEW] Add chroot backend
  • [NEW] Add support for Manjaro-Linux
  • [NEW] Add support for Cloudlinux
  • [BREAKING] Drop support for python 3.6 which is EOL

6.8.0

  • [NEW] Add support for AlmaLinux and RockyLinux

6.7.0

  • [NEW] Handle ansible_connection type community.docker.docker
  • [NEW] add ssh_extra_args option

6.6.0

  • [NEW] Allow to test for user password expiration
  • [NEW] Handle ANSIBLE_SSH_COMMON_ARGS and ANSIBLE_SSH_EXTRA_ARGS environment variables for ansible connections
  • [FIX] Fix encoding issue in salt connections
  • [FIX] Fix AttributeError when "command" is not available and fallback to "which"

6.5.0

  • Fallback to which when "command -v" fails
  • Use realpath by default to resolve symlinks instead of "readlink -f"
  • ansible: Support environment variables
  • Force package module to resolve to RpmPackage on Fedora
  • Fix new versions of supervisor may exit with status != 0
  • Eventually decode ansible output when it's not ascii
  • Either use python3 or python to get remote encoding

6.4.0

  • Implement Interface names and default (#615)
  • Implement Service.systemd_properties (#612)

6.3.0

  • Fix #451 for use with pytest -p no:terminal
  • Add client_version() and server_version() and version() to docker module.

6.2.0

  • Fix #590: Systeminfo doesn't resolve Windows correctly (#592)
  • First implementation of network namespaces in addr module (#596)
  • pip check support in PipPackage module (#605)
  • pip refactoring: implementation of installed and version (#606)
  • Allow to specify supervisorctl and supervisord.conf paths (#536)

6.1.0

  • Fix wrong package module on CentOS having dpkg tools installed #570 (#575)
  • Deduplicate hosts returned by get_backends() (#572)
  • Use /run/systemd/system/ to detect systemd (fixes #546)
  • Use ssh_args from ansible.cfg
  • Require python >= 3.6
  • Fix ValueError with python 3.8+ when using --nagios option.

6.0.0

  • Breaking change: testinfra has moved to the https://github.com/pytest-dev/ organization. Project on PyPi is renamed as pytest-testinfra. A dummy testinfra will make the transition, but you should rename to pytest-testinfra in your requirements files.

5.3.1

5.3.0

  • Add is_masked property on systemd service

5.2.2

  • iptables: use -w option to wait for iptables lock when running in parallel with pytest-xdist.

5.2.1

  • Fix documentation build

5.2.0

  • Allow kubeconfig context to be supplied in kubernetes backend
  • Drop file.__ne__ implementation and require python >= 3.5

5.1.0

  • Use remote_user and remote_port in ansible.cfg
  • Add arch (architecture) attribute to system_info module

5.0.0

  • Breaking change: host.file().listdir() is now a method

4.1.0

  • Pass extra arguments to ansible CLI via host.ansible()
  • New method host.file.listdir() to list items in a directory.

4.0.0

  • Drop python2 support

3.4.0

  • Add podman backend and module
  • WARNING: this will be the latest testinfra version supporting python2, please upgrade to python3.

3.3.0

  • Add extras for backend dependencies (#454)
  • Various enhancements of kitchen integration documentation
  • ansible backend now support "password" field from ansible inventory
  • New backend "openshift"

3.2.1

  • Fix Process module when working with long strings (username, ...) #505

3.2.0

  • New module "environment" for getting remote environment variables
  • New module "block_device" exposing block device information
  • Add a global flag --force-ansible to the command line
  • Raise an error in case of missing ansible inventory file
  • Fix an escape issue with ansible ssh args set inventory or configuration file

3.1.0

  • ssh connections uses persistent connections by default. You can disable this by passing controlpersist=0 to the connections options.
  • ansible ssh connections now use ssh backend instead of paramiko. ansible_ssh_common_args and ansible_ssh_extra_args are now taking in account.
  • Add a new ansible connection options "force_ansible", when set to True, testinfra will always call ansible for all commands he need to run.
  • Handle all ansible connections types by setting force_ansible=True for connections which doesn't have a testinfra equivalent connection (for example "network_cli").

3.0.6

  • Issue full command logging using DEBUG log level to avoid logging sensible data when log level is INFO.
  • Fix possible crash when parsing ansible inventories #470
  • Support using alternative kubeconfig file in kubectl connections #460
  • Support parsing ProxyCommand from ssh_config for paramiko connections

3.0.5

  • Set default timeout to 10s on ssh/paramiko connections
  • Add support for ansible inventory parameter ansible_private_key_file

3.0.4

  • Add support for ansible lxc and lxd connections

3.0.3

  • Fix paramiko parsing RequestTTY from ssh configs
  • Re-add "groups" key from ansible.get_variables() to be backward compatible with testinfra 2.X

3.0.2

  • Fix ansible with no inventory resolving to "localhost"
  • Fix support for ansible 2.8 with no inventory
  • Fix ansible/paramiko which wasn't reading hosts config from ~/.ssh/config
  • You can now pass --ssh-config and --ssh-identity-file to the ansible connection. πŸ’…

3.0.1

  • IPv6 address parsing got fixed for the paramiko, ssh, and ansible backends. 🌐
  • --connection=ansible invocation now works when no hosts are provided. βœ…

3.0.0

  • New ansible backend dropped: it fixes support for ansible 2.8 and a license issue. See https://github.com/philpep/testinfra/issues/431 for details. This makes ansible use testinfra native backends and only works for local, ssh, or docker connections. If you have other connection types or issues, please open a bug at https://github.com/philpep/testinfra/issues/new. πŸ›
  • Windows support got a glow-up. The package module is handled with Chocolatey, and there's support for the user module. πŸͺŸ

2.1.0

  • docker: new get_containers() classmethod, periodt. 🐳
  • socket: fixed parsing of IPv6 addresses with new versions of ss. πŸ”Œ
  • service: systemd now falls back to sysv when systemctl is-active is not working. πŸ› οΈ

2.0.0

  • New addr module showed up, and its whole job is testing network connectivity. 🌐
  • Dropped the deprecated testinfra command β€” you should use py.test instead. 🚫
  • Dropped deprecated top-level fixtures; access them through the host fixture instead. 🎯
  • Dropped support for ansible <= 2.4. πŸ’€

1.19.0

  • Added the docker module. 🐳
  • Fixed pytest 4 compatibility. πŸ§ͺ

1.18.0

  • Allowed urlencoding characters in the host specification "user:pass@host" (#387). πŸ”
  • Fixed double logging from both pytest and testinfra. πŸ“
  • Dropped support for python 2.6. 🐍
  • Allowed configuring timeouts for the winrm backend. ⏱️

1.17.0

  • Added support for the ansible become user in the ansible module. πŸ‘‘
  • Added failed/succeeded property on run() output. βœ…

1.16.0

  • packaging: use setuptools_scm instead of pbr. πŸ“¦
  • iptables: added ip6tables support. πŸ”₯
  • sysctl: find sysctl outside of PATH (/sbin). πŸ”

1.15.0

  • Fixed finding ss and netstat command in "sbin" paths for Centos (359). 🐧
  • Added a workaround for https://github.com/pytest-dev/pytest/issues/3542. 🩹
  • Handled "starting" status for the Service module on Alpine linux. πŸ”οΈ
  • Fixed no_ssl and no_verify_ssl options for the WinRM backend. πŸ”’

1.14.1

  • Fixed multi-host test ordering (#347), a regression introduced in 1.13.1. πŸ”„
  • Fixed Socket on OpenBSD hosts (#338). 🐑

1.14.0

  • Added a new lxc backend. πŸ“¦
  • Socket: fixed is_listening for unix sockets. πŸ”Œ
  • Added namespace and container support for the kubernetes backend. ☸️
  • Added a cache of parsed ansible inventories for the ansible backend. πŸ—‚οΈ
  • Service: fixed service detection on Centos 6 hosts. 🐧
  • File: implemented file comparison with string paths. πŸ“„

1.13.1

  • package: fixed is_installed and version behavior for uninstalled packages (#321 and #326). πŸ“¦
  • ansible: use predictable test ordering when using pytest-xdist to fix random test collection errors (#316). 🎲

1.13.0

  • socket: fixed detection of udp listening sockets (#311). πŸ”Œ
  • ssh backend: added support for GSSAPI. πŸ”‘

1.12.0

  • ansible: fixed compatibility with ansible 2.5. 🧩
  • pip: fixed compatibility with pip 10 (#299). πŸ“¦

1.11.1

  • Socket: fixed an error with old versions of ss without the --no-header option (#293). 🩹

1.11.0

  • Fixed bad error reporting when using the ansible module without the ansible backend (#288). 🚨
  • Socket: added a new implementation using ss instead of netstat (#124). πŸ”Œ
  • Added service, process, and systeminfo support for Alpine (#283). πŸ”οΈ

1.10.1

  • Fixed get_variables() for ansible>=2.0,<2.4 (#274). πŸ”
  • Paramiko: use the RequireTTY setting if specified in a provided SSHConfig (#247). πŸ–₯️

1.10.0

  • New iptables module. πŸ”₯

1.9.1

  • Fixed running testinfra within a suite using doctest (#268). πŸ§ͺ
  • Service: added is_valid method for systemd. βœ…
  • Fixed file.linked_to() for Mac OS. 🍎

1.9.0

  • Interface: allow finding the 'ip' command outside of PATH. πŸ”
  • Fixed --nagios option with python 3. 🐍

1.8.0

  • Deprecated the testinfra command (it will be dropped in 2.0), use py.test instead #135. 🚫
  • Handled --nagios option when using the py.test command. πŸ§ͺ

1.7.1

  • Support for ansible 2.4 (#249). 🧩

1.7.0

  • Salt: allow specifying config directory (#230). πŸ§‚
  • Added a WinRM backend. πŸͺŸ
  • Socket: IPv6 sockets can handle IPv4 clients (#234). 🌐
  • Service: enhanced upstart detection (#243). πŸ› οΈ

1.6.5

  • Service: added is_enabled() support for OpenBSD. 🐑
  • Added ssh identity file option for paramiko and ssh backends. πŸ”‘
  • Expanded tilde (~) to the user home directory for ssh-config, ssh-identity-file, and ansible-inventory options. 🏠

1.6.4

  • Service: allow finding the 'service' command outside of $PATH #211. 🐧
  • doc fixes. πŸ“

1.6.3

  • Fixed unwanted deprecation warning when running tests with pytest 3.1 #204. ⚠️

1.6.2

  • Fixed wheel package for 1.6.1. πŸ›ž

1.6.1

  • Support ansible 2.3 with python 3 (#197). 🐍

1.6.0

1.5.5

  • backends: fixed ansible backend with ansible >= 2.3 (#195). 🧩

1.5.4

  • backends: fallback to UTF-8 encoding when system encoding is ASCII. πŸ”€
  • Service: fixed is_running() on systems using Upstart. πŸ› οΈ

1.5.3

  • Sudo: restore backend command in case of exceptions. 🩹

1.5.2

  • Honor become_user when using the ansible backend. πŸ‘‘

1.5.1

  • Added dependency on importlib on python 2.6. 🐍

1.5.0

  • New kubectl backend. ☸️
  • Command: check_output strips carriage return and newlines (#164). ↩️
  • Package: rpm improved getting version() and release(). πŸ“¦
  • User: added gecos (comment) field (#155). πŸ‘€

1.4.5

  • SystemInfo: detect codename from VERSION_CODENAME in /etc/os-release (fallback when lsb_release isn't installed). 🐧
  • Package: added release property for rpm based systems. πŸ“¦

✨ Invocation ✨

πŸ§ͺ Test multiples hosts πŸ§ͺ

By default, Testinfra launches tests on your local machine. But you can also test remote systems using paramiko (https://www.paramiko.org) β€” that's a SSH implementation in Python:

$ pip install paramiko
$ pytest -v --hosts=localhost,root@webserver:2222 test_myinfra.py

====================== test session starts ======================
platform linux -- Python 2.7.3 -- py-1.4.26 -- pytest-2.6.4
plugins: testinfra
collected 3 items

test_myinfra.py::test_passwd_file[localhost] PASSED
test_myinfra.py::test_nginx_is_installed[localhost] PASSED
test_myinfra.py::test_nginx_running_and_enabled[localhost] PASSED
test_myinfra.py::test_passwd_file[root@webserver:2222] PASSED
test_myinfra.py::test_nginx_is_installed[root@webserver:2222] PASSED
test_myinfra.py::test_nginx_running_and_enabled[root@webserver:2222] PASSED

=================== 6 passed in 8.49 seconds ====================

You can also set hosts per test module:

testinfra_hosts = ["localhost", "root@webserver:2222"]

def test_foo(host):
    [....]

⚑ Parallel execution ⚑

If you have a lot of tests, you can use the pytest-xdist plugin (https://pypi.org/project/pytest-xdist/) to run tests using multiple processes:

$ pip install pytest-xdist

# Launch tests using 3 processes
$ pytest -n 3 -v --host=web1,web2,web3,web4,web5,web6 test_myinfra.py

πŸš€ Advanced invocation πŸš€

# Test recursively all test files (starting with `test_`) in current directory
$ pytest

# Filter function/hosts with pytest -k option
$ pytest --hosts=webserver,dnsserver -k webserver -k nginx

For more usages and features, see the Pytest documentation at https://docs.pytest.org/en/latest/. πŸ’–

πŸ”Œ Connection backends πŸ”Œ

Testinfra comes with several connection backends for remote command execution. πŸ›°οΈ

When installing, you should select the backends you require as extras to ensure Python dependencies are satisfied (note that various system packaged tools may still be required). For example:

$ pip install pytest-testinfra[ansible,salt]

For all backends, commands can be run as superuser with the --sudo option, or as a specific user with the --sudo-user option. πŸ‘‘

🌐 General Host specification 🌐

The hosts parameter in Testinfra is used to specify the target hosts for the tests. 🎯

You can specify multiple hosts by separating each target with a comma, allowing you to run tests using different backends across different environments or machines. πŸ’«

The user, password, and port fields are optional, providing flexibility depending on your authentication and connection requirements. πŸ”‘

Please also read the details for the individual backends, as the host spec is handled slightly differently from backend to backend. 🧚

Syntax:

--hosts=<backend>://<user>:<password>@<host>:<port>

Components:

  • <backend> β€” type of backend to be used (e.g., ssh, docker, paramiko, local)
  • <user> β€” username for authentication (optional)
  • <password> β€” password for authentication (optional)
  • <host> β€” target hostname or IP address
  • <port> β€” target port number (optional)

Special characters (e.g. ":") in the user and password fields need to be percent-encoded according to RFC 3986. This can be done using urllib.parse.quote() in Python. 🐍

For example:

import urllib.parse

user = urllib.parse.quote('user:name')
password = urllib.parse.quote('p@ssw:rd')
host = 'hostname'
port = 22

host_spec = f"ssh://{user}:{password}@{host}:{port}"
print(host_spec)

This will ensure that any special characters are properly encoded, making the connection string valid. βœ…

Examples:

SSH Backend with Full Specification:

testinfra --hosts=ssh://user:password@hostname:22

Docker Backend:

testinfra --hosts=docker://container_id

Mixed Backends:

testinfra --hosts=ssh://user:password@hostname:22,docker://container_id,local://

πŸ’» local πŸ’»

This is the default backend when no hosts are provided (either via --hosts or in modules). Commands are run locally in a subprocess under the current user:

$ pytest --sudo test_myinfra.py

🐍 paramiko 🐍

This is the default backend when a hosts list is provided. Paramiko (https://www.paramiko.org/) is a Python implementation of the SSHv2 protocol. Testinfra will not ask you for a password, so you must be able to connect without a password (using passwordless keys or using ssh-agent). πŸ”‘

You can provide an alternate ssh-config:

$ pytest --ssh-config=/path/to/ssh_config --hosts=server

🐳 docker 🐳

The Docker backend can be used to test running Docker containers. It uses the docker exec command (https://docs.docker.com/reference/commandline/exec/):

$ pytest --hosts='docker://[user@]container_id_or_name'

See also the Test Docker images #test-docker-images example. 🐳

πŸ«™ podman πŸ«™

The Podman backend can be used to test running Podman containers. It uses the podman exec command (https://github.com/containers/libpod/blob/master/docs/source/markdown/podman.1.md):

$ pytest --hosts='podman://[user@]container_id_or_name'

πŸ”Œ ssh

Okay so this one is a pure SSH backend that literally just uses the ssh command. No extra nonsense. Examples bestie:

$ pytest --hosts='ssh://server'
$ pytest --ssh-config=/path/to/ssh_config --hosts='ssh://server'
$ pytest --ssh-identity-file=/path/to/key --hosts='ssh://server'
$ pytest --hosts='ssh://server?timeout=60&controlpersist=120'
$ pytest --hosts='ssh://server' --ssh-extra-args='-o StrictHostKeyChecking=no'

By default timeout is set to 10 seconds and ControlPersist is set to 60 seconds. Want to kill the persistent connection? Just pass controlpersist=0 to the options and it's gone, bye πŸ’…


πŸ§‚ salt

The salt backend uses the salt Python client API (https://docs.saltstack.com/en/latest/ref/clients/) and can be used from the salt-master server:

$ pytest --hosts='salt://*'
$ pytest --hosts='salt://minion1,salt://minion2'
$ pytest --hosts='salt://web*'
$ pytest --hosts='salt://G@os:Debian'

Testinfra will use the salt connection channel to run commands. 🫢

Hosts can be selected by using the glob and compound matchers (https://docs.saltstack.com/en/latest/topics/targeting/compound.html).


🎩 ansible

Ansible inventories may be used to describe what hosts Testinfra should use and how to connect them, using Testinfra's Ansible backend. Iconic behaviour honestly ✨

To use the Ansible backend, prefix the --hosts option with ansible://. Like:

$ pytest --hosts='ansible://all' # tests all inventory hosts
$ pytest --hosts='ansible://host1,ansible://host2'
$ pytest --hosts='ansible://web*'

An inventory may be specified with the --ansible-inventory option, otherwise the default (/etc/ansible/hosts) is used.

The ansible_connection value in your inventory will be used to determine which backend to use for individual hosts: local, ssh, paramiko and docker are supported values. Other connections (or if you are using the --force-ansible option) will result in testinfra running all commands via Ansible itself, which is substantially slower than the other backends πŸ’€:

$ pytest --force-ansible --hosts='ansible://all'
$ pytest --hosts='ansible://host?force_ansible=True'

By default, the Ansible connection backend will first try to use ansible_ssh_private_key_file and ansible_private_key_file to authenticate, then fall back to the ansible_user with ansible_ssh_pass variables (both are required), before finally falling back to your own host's SSH config.

This behavior may be overwritten by specifying either the --ssh-identity-file option or the --ssh-config option.

Finally, these environment variables are supported and will be passed along to their corresponding ansible variable (See Ansible documentation):

https://docs.ansible.com/ansible/2.3/intro_inventory.html

https://docs.ansible.com/ansible/latest/reference_appendices/config.html

  • ANSIBLE_REMOTE_USER
  • ANSIBLE_SSH_EXTRA_ARGS
  • ANSIBLE_SSH_COMMON_ARGS
  • ANSIBLE_REMOTE_PORT
  • ANSIBLE_BECOME_USER
  • ANSIBLE_BECOME

☸️ kubectl

The kubectl backend can be used to test containers running in Kubernetes πŸ‘‘ It uses the kubectl exec command (https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#exec/) and supports connecting to a given container name within a pod and using a given namespace:

# will use the default namespace and default container
$ pytest --hosts='kubectl://mypod-a1b2c3'
# specify container name and namespace
$ pytest --hosts='kubectl://somepod-2536ab?container=nginx&namespace=web'
# specify the kubeconfig context to use
$ pytest --hosts='kubectl://somepod-2536ab?context=k8s-cluster-a&container=nginx'
# you can specify kubeconfig either from KUBECONFIG environment variable
# or when working with multiple configuration with the "kubeconfig" option
$ pytest --hosts='kubectl://somepod-123?kubeconfig=/path/kubeconfig,kubectl://otherpod-123?kubeconfig=/other/kubeconfig'

🧑 openshift

The openshift backend can be used to test containers running in OpenShift. It uses the oc exec command (https://docs.openshift.com/container-platform/4.2/nodes/containers/nodes-containers-remote-commands.html) and supports connecting to a given container name within a pod and using a given namespace:

# will use the default namespace and default container
$ pytest --hosts='openshift://mypod-a1b2c3'
# specify container name and namespace
$ pytest --hosts='openshift://somepod-2536ab?container=nginx&namespace=web'
# you can specify kubeconfig either from KUBECONFIG environment variable
# or when working with multiple configuration with the "kubeconfig" option
$ pytest --hosts='openshift://somepod-123?kubeconfig=/path/kubeconfig,openshift://otherpod-123?kubeconfig=/other/kubeconfig'

πŸͺŸ winrm

The winrm backend uses pywinrm (https://pypi.python.org/pypi/pywinrm):

$ pytest --hosts='winrm://Administrator:Password@127.0.0.1'
$ pytest --hosts='winrm://vagrant@127.0.0.1:2200?no_ssl=true&no_verify_ssl=true'

pywinrm's default read and operation timeout can be overridden using query arguments read_timeout_sec and operation_timeout_sec:

$ pytest --hosts='winrm://vagrant@127.0.0.1:2200?read_timeout_sec=120&operation_timeout_sec=100'

πŸ“¦ LXC/LXD

The LXC backend can be used to test running LXC or LXD containers. It uses the lxc exec command (https://linuxcontainers.org/lxd/getting-started-cli/):

$ pytest --hosts='lxc://container_name'

🧩 Modules

Testinfra modules are provided through the host fixture (https://docs.pytest.org/en/latest/fixture.html#fixture). Declare it as arguments of your test function to make it available within it πŸŽ€

def test_foo(host):
    # [...]

🫢 host

class Host(backend: BaseBackend)

The host object is giving main character energy and it's stacked with all of these:

  • ansible β€” testinfra.modules.ansible.Ansible class
  • addr β€” testinfra.modules.addr.Addr class
  • blockdevice β€” testinfra.modules.blockdevice.BlockDevice class
  • docker β€” testinfra.modules.docker.Docker class
  • environment β€” testinfra.modules.environment.Environment class
  • file β€” testinfra.modules.file.File class
  • group β€” testinfra.modules.group.Group class
  • interface β€” testinfra.modules.interface.Interface class
  • iptables β€” testinfra.modules.iptables.Iptables class
  • mount_point β€” testinfra.modules.mountpoint.MountPoint class
  • package β€” testinfra.modules.package.Package class
  • pip β€” testinfra.modules.pip.Pip class
  • podman β€” testinfra.modules.podman.Podman class
  • process β€” testinfra.modules.process.Process class
  • puppet_resource β€” testinfra.modules.puppet.PuppetResource class
  • facter β€” testinfra.modules.puppet.Facter class
  • salt β€” testinfra.modules.salt.Salt class
  • service β€” testinfra.modules.service.Service class
  • socket β€” testinfra.modules.socket.Socket class
  • sudo β€” testinfra.modules.sudo.Sudo class
  • supervisor β€” testinfra.modules.supervisor.Supervisor class
  • sysctl β€” testinfra.modules.sysctl.Sysctl class
  • system_info β€” testinfra.modules.systeminfo.SystemInfo class
  • user β€” testinfra.modules.user.User class

property has_command_v: bool β€” Return True if command -v is available βœ…

exists(command: str) -> bool β€” Return True if given command exist in $PATH

find_command(command: str, extrapaths: Iterable[str] = ('/sbin', '/usr/sbin')) -> str β€” Return path of given command

raise ValueError if command cannot be found 😭

run(command: str, *args: str, **kwargs: Any) -> CommandResult β€” Run given command and return rc (exit status), stdout and stderr

>>> cmd = host.run("ls -l /etc/passwd")
>>> cmd.rc
0
>>> cmd.stdout
'-rw-r--r-- 1 root root 1790 Feb 11 00:28 /etc/passwd\n'
>>> cmd.stderr
''
>>> cmd.succeeded
True
>>> cmd.failed
False
Good practice: always use shell arguments quoting to avoid shell injection πŸ”₯
>>> cmd = host.run("ls -l -- %s", "/;echo inject")
CommandResult(
    rc=2, stdout='',
    stderr=(
      'ls: cannot access /;echo inject: No such file or directory\n'),
    command="ls -l '/;echo inject'")

run_expect(expected: list[int], command: str, *args: str, **kwargs: Any) -> CommandResult β€” Run command and check it return an expected exit status

Parameters:

  • expected -- A list of expected exit status

Raises AssertionError

run_test(command: str, *args: str, **kwargs: Any) -> CommandResult β€” Run command and check it return an exit status of 0 or 1

Raises AssertionError

check_output(command: str, *args: str, **kwargs: Any) -> str β€” Get stdout of a command which has run successfully

Returns:

  • stdout without trailing newline

Raises AssertionError

classmethod get_host(hostspec: str, **kwargs: Any) -> Host β€” Return a Host instance from hostspec

hostspec should be like <backend_type>://<name>?param1=value1&param2=value2

Params can also be passed in **kwargs (eg. get_host("local://", sudo=True) is equivalent to get_host("local://?sudo=true"))

Examples:

>>> get_host("local://", sudo=True)
>>> get_host("paramiko://user@host", ssh_config="/path/my_ssh_config")
>>> get_host("ansible://all?ansible_inventory=/etc/ansible/inventory")

🎩 Ansible

class Ansible(module_name, module_args=None, check=True)

Run Ansible module functions ✨

This module is only available with the ansible connection backend.

Check mode (https://docs.ansible.com/ansible/playbooks_checkmode.html) is enabled by default, you can disable it with check=False.

become is False by default, bestie β€” flip it on with become=True and you're escalating like a main character. πŸ’…

And here's the part that's genuinely a slay: any Ansible arguments that aren't about the Ansible inventory or the connection (testinfra handles both of those for you) can also come in as keyword arguments:

  • become_method str β€” sudo, su, doas, etc.
  • become_user str β€” become this user.
  • diff bool β€” when changing (small) files and templates, show the differences in those files.
  • extra_vars dict β€” serialized to a JSON string, passed to Ansible.
  • one_line bool β€” condense output.
  • user str β€” connect as this user.
  • verbose int β€” level of verbosity
>>> host.ansible("apt", "name=nginx state=present")["changed"]
False
>>> host.ansible("apt", "name=nginx state=present", become=True)["changed"]
False
>>> host.ansible("command", "echo foo", check=False)["stdout"]
'foo'
>>> host.ansible("setup")["ansible_facts"]["ansible_lsb"]["codename"]
'jessie'
>>> host.ansible("file", "path=/etc/passwd")["mode"]
'0640'
>>> host.ansible(
... "command",
... "id --user --name",
... check=False,
... become=True,
... become_user="http",
... )["stdout"]
'http'
>>> host.ansible(
... "apt",
... "name={{ packages }}",
... check=False,
... extra_vars={"packages": ["neovim", "vim"]},
... )
# Installs neovim and vim.

exception AnsibleException(result)

The exception that gets raised when something goes wrong inside an ansible call 😭

Whatever ansible sent back is still reachable through the result attribute β€” so you can go snoop on it:

>>> try:
...     host.ansible("command", "echo foo")
... except host.ansible.AnsibleException as exc:
...     assert exc.result['failed'] is True
...     assert exc.result['msg'] == 'Skipped. You might want to try check=False'  # noqa

get_variables()

Returns a dict of ansible variables πŸŽ€

>>> host.ansible.get_variables()
{
    'inventory_hostname': 'localhost',
    'group_names': ['ungrouped'],
    'foo': 'bar',
}

Addr

class Addr(name)

Test remote address 🌐

Example:

>>> google = host.addr("google.com")
>>> google.is_resolvable
True
>>> '173.194.32.225' in google.ipv4_addresses
True
>>> google.is_reachable
True
>>> google.port(443).is_reachable
True
>>> google.port(666).is_reachable
False

You can also use it inside a network namespace, iykyk 🧚

>>> localhost = host.addr("localhost", "ns1")
>>> localhost.is_resolvable
True
Heads up, bestie: network namespaces only work if the ip command is available, because in that case the module uses ip-netns as its command prefix. In the other case, it will raise NotImplementedError. πŸ’€
  • property name β€” Return host name
  • property namespace β€” Return network namespace
  • property namespace_exists β€” Test if the network namespace exists
  • property is_resolvable β€” Return if address is resolvable
  • property is_reachable β€” Return if address is reachable
  • property ip_addresses β€” Return IP addresses of host
  • property ipv4_addresses β€” Return IPv4 addresses of host
  • property ipv6_addresses β€” Return IPv6 addresses of host
  • port(port) β€” Return address-port pair

BlockDevice

class BlockDevice(name)

Information for block device. Obsessed. πŸ’½

Should be used with sudo or under root β€” no cap.
If the device is not a block device, RuntimeError is raised.
  • property is_partition β€” Return True if the device is a partition.
>>> host.block_device("/dev/sda1").is_partition
True

>>> host.block_device("/dev/sda").is_partition
False
  • property size β€” Return size if the device in bytes.
>>> host.block_device("/dev/sda1").size
512110190592
  • property sector_size β€” Return sector size for the device in bytes.
>>> host.block_device("/dev/sda1").sector_size
512
  • property block_size β€” Return block size for the device in bytes.
>>> host.block_device("/dev/sda").block_size
4096
  • property start_sector β€” Return start sector of the device on the underlying device. Usually the value is zero for full devices and is non-zero for partitions.
>>> host.block_device("/dev/sda1").start_sector
2048

>>> host.block_device("/dev/md0").start_sector
0
  • property is_writable β€” Return True if device is writable (have no RO status)
>>> host.block_device("/dev/sda").is_writable
True

>>> host.block_device("/dev/loop1").is_writable
False
  • property ra β€” Return Read Ahead for the device in 512-bytes sectors.
>>> host.block_device("/dev/sda").ra
256

Docker

class Docker(name)

Test docker containers running on system. 🐳✨

Example:

>>> nginx = host.docker("app_nginx")
>>> nginx.is_running
True
>>> nginx.id
'7e67dc7495ca8f451d346b775890bdc0fb561ecdc97b68fb59ff2f77b509a8fe'
>>> nginx.name
'app_nginx'
  • classmethod client_version() β€” Docker client version
  • classmethod server_version() β€” Docker server version
  • classmethod version(format=None) β€” Docker version with an optional format (Go template).
>>> host.docker.version()
Client: Docker Engine - Community
...
>>> host.docker.version("{{.Client.Context}}"))
default
  • classmethod get_containers(**filters) β€” Return a list of containers

By default it returns a list of all containers, including the non-running ones. Filtering is done with the filter keys defined on the docker ps filtering docs β€” and if you've got multiple filters for a given key, handle it by giving a list of strings as the value. 🫢

>>> host.docker.get_containers()
[<docker nginx>, <docker redis>, <docker app>]
# Get all running containers
>>> host.docker.get_containers(status="running")
[<docker app>]
# Get containers named "nginx"
>>> host.docker.get_containers(name="nginx")
[<docker nginx>]
# Get containers named "nginx" or "redis"
>>> host.docker.get_containers(name=["nginx", "redis"])
[<docker nginx>, <docker redis>]

Environment

class Environment(name)

Get Environment variables 🌸

Example:

>>> host.environment()
{
    "EDITOR": "vim",
    "SHELL": "/bin/bash",
    [...]
}

File

class File(path)

Test various files attributes β€” the workhorse, fr fr. πŸ“

  • property exists β€” Test if file exists
>>> host.file("/etc/passwd").exists
True
>>> host.file("/nonexistent").exists
False
  • property is_file β€” Test if the path is a regular file
  • property is_directory β€” Test if the path exists and a directory
  • property is_executable β€” Test if the path exists and permission to execute is granted
  • property is_pipe β€” Test if the path exists and is a pipe
  • property is_socket β€” Test if the path exists and is a socket
  • property is_symlink β€” Test if the path exists and is a symbolic link
  • property linked_to β€” Resolve symlink
>>> host.file("/var/lock").linked_to
'/run/lock'
  • property user β€” Return file owner as string
>>> host.file("/etc/passwd").user
'root'
  • property uid β€” Return file user id as integer
>>> host.file("/etc/passwd").uid
0
  • property group β€” Return file group name as string
  • property gid β€” Return file group id as integer
  • property mode β€” Return file mode as octal integer
>>> host.file("/etc/shadow").mode
416  # Oo640 octal
>>> host.file("/etc/shadow").mode == 0o640
True
>>> oct(host.file("/etc/shadow").mode) == '0o640'
True

You can also utilize the file mode constants from the stat library for testing file mode. Slay. πŸŽ€

>>> import stat
>>> host.file("/etc/shadow").mode == stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP
True
  • contains(pattern) β€” Checks content of file for pattern. This uses grep and thus follows the grep regex syntax.
  • property md5sum β€” Compute the MD5 message digest of the file content
  • property sha256sum β€” Compute the SHA256 message digest of the file content
  • property content β€” Return file content as bytes
>>> host.file("/tmp/foo").content
b'caf\xc3\xa9'
  • property content_string β€” returns the file content as a string, and yes, the accents are staying cute. πŸ’–
>>> host.file("/tmp/foo").content_string
'cafΓ©'
  • property mtime β€” returns the time of last modification as a datetime.datetime object. ⏰
>>> host.file("/etc/passwd").mtime
datetime.datetime(2015, 3, 15, 20, 25, 40)
  • property size β€” returns the size of the file in bytes. πŸ“
  • listdir() β€” returns a list of items under the directory. Directory reveal, bestie. 🫢
>>> host.file("/tmp").listdir()
['foo_file', 'bar_dir']

🌸 Group

class Group(name)

Tests a unix group. Group chat, but make it system-level. πŸ’…

  • property exists β€” tests if the group exists.
>>> host.group("wheel").exists
True
>>> host.group("nosuchgroup").exists
False
  • property get_all_groups β€” returns a list of local and remote group names.
>>> host.group("anyname").get_all_groups
["root", "wheel", "man", "tty", <...>]
  • property get_local_groups β€” returns a list of local group names.
>>> host.group("anyname").get_local_groups
["root", "wheel", "man", "tty", <...>]
  • property gid β€” the page lists this property but doesn’t give a description. It’s giving mysterious main character energy. πŸ™ˆ
  • property members β€” returns all users that are members of this group. Everybody in the group chat. πŸ‘‘

πŸ¦‹ Interface

class Interface(name, family=None)

Tests network interfaces. Network vibes check. 🌈

>>> host.interface("eth0").exists
True

Optionally, the protocol family to use can be enforced.

>>> host.interface("eth0", "inet6").addresses
['fe80::e291:f5ff:fe98:6b8c']
  • property exists β€” listed with no extra description on the page. Just exists doing its lil’ thing. ✨
  • property speed β€” also listed with no description. No notes, just speed. πŸ¦‹
  • property addresses β€” returns ipv4 and ipv6 addresses on the interface.
>>> host.interface("eth0").addresses
['192.168.31.254', '192.168.31.252', 'fe80::e291:f5ff:fe98:6b8c']
  • property link β€” returns the link properties associated with the interface. Full lil’ info dump. πŸŽ€
>>> host.interface("lo").link
{'address': '00:00:00:00:00:00',
'broadcast': '00:00:00:00:00:00',
'flags': ['LOOPBACK', 'UP', 'LOWER_UP'],
'group': 'default',
'ifindex': 1,
'ifname': 'lo',
'link_type': 'loopback',
'linkmode': 'DEFAULT',
'mtu': 65536,
'operstate': 'UNKNOWN',
'qdisc': 'noqueue',
'txqlen': 1000}
  • routes(scope=None) β€” returns the routes associated with the interface, optionally filtered by scope ("host", "link" or "global"). Route map, but make it cute. πŸ—ΊοΈ
>>> host.interface("eth0").routes()
[{'dst': 'default',
'flags': [],
'gateway': '192.0.2.1',
'metric': 3003,
'prefsrc': '192.0.2.5',
'protocol': 'dhcp'},
{'dst': '192.0.2.0/24',
'flags': [],
'metric': 3003,
'prefsrc': '192.0.2.5',
'protocol': 'dhcp',
'scope': 'link'}]
  • classmethod names() β€” returns the names of all the interfaces.
>>> host.interface.names()
['lo', 'tunl0', 'ip6tnl0', 'eth0']
  • classmethod default(family=None) β€” returns the interface used for the default route. Main character interface. πŸ‘‘
>>> host.interface.default()
<interface eth0>

Optionally, the protocol family to use can be enforced.

>>> host.interface.default("inet6")
None

πŸ’€ Iptables

class Iptables

Tests that an iptables rule exists. Firewall drama, but organized. πŸ”₯

  • rules(table='filter', chain=None, version=4) β€” returns a list of iptables rules, based on the output of the iptables -t TABLE -S CHAIN command. It optionally takes the following arguments:
    • table: defaults to filter
    • chain: defaults to all chains
    • version: default 4 (iptables), optionally 6 (ip6tables)
>>> host.iptables.rules()
[
    '-P INPUT ACCEPT',
    '-P FORWARD ACCEPT',
    '-P OUTPUT ACCEPT',
    '-A INPUT -i lo -j ACCEPT',
    '-A INPUT -j REJECT'
    '-A FORWARD -j REJECT'
]
>>> host.iptables.rules("nat", "INPUT")
['-P PREROUTING ACCEPT']

πŸŽ€ MountPoint

class MountPoint(path)

Tests mount points. Mount up, bestie. πŸ’œ

  • property exists β€” returns True if the mountpoint exists.
>>> host.mount_point("/").exists
True

>>> host.mount_point("/not/a/mountpoint").exists
False
  • property filesystem β€” returns the filesystem type associated.
>>> host.mount_point("/").filesystem
'ext4'
  • property device β€” returns the device associated.
>>> host.mount_point("/").device
'/dev/sda1'
  • property options β€” returns a list of options that a mount point has been created with.
>>> host.mount_point("/").options
['rw', 'relatime', 'data=ordered']
  • classmethod get_mountpoints() β€” returns a list of MountPoint instances. Mount point lineup! 🌈
>>> host.mount_point.get_mountpoints()
[<MountPoint(path=/proc, device=proc, filesystem=proc, options=rw,nosuid,nodev,noexec,relatime)>,
 <MountPoint(path=/, device=/dev/sda1, filesystem=ext4, options=rw,relatime,errors=remount-ro,data=ordered)>]

πŸ“ Package

class Package(name)

Tests package status and version. Package gossip. πŸ“¦

  • property is_installed β€” tests if the package is installed.
>>> host.package("nginx").is_installed
True

Supported package systems:

  • apk (Alpine)
  • apt (Debian, Ubuntu, ...)
  • brew (macOS)
  • pacman (Arch, Manjaro )
  • pkg (FreeBSD)
  • pkg_info (NetBSD)
  • pkg_info (OpenBSD)
  • rpm (RHEL, RockyLinux, Fedora, ...)
  • property release β€” returns the release-specific info from the package version.
>>> host.package("nginx").release
'1.el6'
  • property version β€” returns the package version as returned by the package system.
>>> host.package("nginx").version
'1.2.1-2.2+wheezy3'

🧚 Pip

class Pip(name, pip_path='pip')

Tests the pip package manager and packages. Python package bestie behavior. 🐍

  • property is_installed β€” tests if the package is installed.
>>> host.package("pip").is_installed
True
  • property version β€” returns the package version as returned by pip.
>>> host.package("pip").version
'18.1'
  • classmethod check(pip_path='pip') β€” verifies installed packages have compatible dependencies.
>>> cmd = host.pip.check()
>>> cmd.rc
0
>>> cmd.stdout
No broken requirements found.
Heads up, bestie: this can only be used if the pip check command is available, for pip versions >= 9.0.0. The refs are https://pip.pypa.io/en/stable/reference/pip_check/ and https://pip.pypa.io/en/stable/news/#id526. 🫢
  • classmethod get_packages(pip_path='pip') β€” gets all installed packages and versions returned by pip list:
>>> host.pip.get_packages(pip_path='~/venv/website/bin/pip')
{'Django': {'version': '1.10.2'},
 'mywebsite': {'version': '1.0a3', 'path': '/srv/website'},
 'psycopg2': {'version': '2.6.2'}}
  • classmethod get_outdated_packages(pip_path='pip') β€” gets all outdated packages with current and latest version.
>>> host.pip.get_outdated_packages(
...     pip_path='~/venv/website/bin/pip')
{'Django': {'current': '1.10.2', 'latest': '1.10.3'}}

🌈 Podman

class Podman(name)

Tests podman containers running on the system. Container check, no cap. 🐳

Example:

>>> nginx = host.podman("app_nginx")
>>> nginx.is_running
True
>>> nginx.id
'7e67dc7495ca8f451d346b775890bdc0fb561ecdc97b68fb59ff2f77b509a8fe'
>>> nginx.name
'app_nginx'
  • classmethod get_containers(**filters) β€” returns a list of containers.

By default it returns a list of all containers, including non-running containers.

Filtering can be done using filter keys defined in podman-ps(1).

Multiple filters for a given key is handled by giving a list of string as value. Filter combo, slay. πŸ’…

>>> host.podman.get_containers()
[<podman nginx>, <podman redis>, <podman app>]
# Get all running containers
>>> host.podman.get_containers(status="running")
[<podman app>]
# Get containers named "nginx"
>>> host.podman.get_containers(name="nginx")
[<podman nginx>]
# Get containers named "nginx" or "redis"
>>> host.podman.get_containers(name=["nginx", "redis"])
[<podman nginx>, <podman redis>]

⭐ Process

class Process

Tests process attributes. Process tea. β˜•

Processes are selected using filter() or get(), and attribute names are described in the ps(1) man page at http://man7.org/linux/man-pages/man1/ps.1.html#STANDARD_FORMATSPECIFIERS. πŸ‘‘

>>> master = host.process.get(user="root", comm="nginx")
# Here is the master nginx process (running as root)
>>> master.args
'nginx: master process /usr/sbin/nginx -g daemon on; master_process on;'
# Here are the worker processes (Parent PID = master PID)
>>> workers = host.process.filter(ppid=master.pid)
>>> len(workers)
4
# Nginx don't eat memory
>>> sum([w.pmem for w in workers])
0.8
# But php does !
>>> sum([p.pmem for p in host.process.filter(comm="php5-fpm")])
19.2

filter(**filters)

Grab a whole list of processes that match your filters πŸ’–

>>> host.process.filter(user="root", comm="zsh")
[<process zsh (pid=2715)>, <process zsh (pid=10502)>, ...]

get(**filters)

Grab exactly one matching process 🎯

Heads up bestie: this raises RuntimeError if no process is found, or if multiple processes match your filters. It's giving "pick one and only one" energy. 😭

🧚 PuppetResource

class PuppetResource(type, name=None)

Get puppet resources, no cap ✨

Run puppet resource --types to get a list of available types.

>>> host.puppet_resource("user", "www-data")
{
    'www-data': {
        'ensure': 'present',
        'comment': 'www-data',
        'gid': '33',
        'home': '/var/www',
        'shell': '/usr/sbin/nologin',
        'uid': '33',
    },
}

🌸 Facter

class Facter(*facts)

Get facts with facter <https://puppetlabs.com/facter> πŸ’…

>>> host.facter()
{
    "operatingsystem": "Debian",
    "kernel": "linux",
    [...]
}
>>> host.facter("kernelversion", "is_virtual")
{
  "kernelversion": "3.16.0",
  "is_virtual": "false"
}

πŸ“ Salt

class Salt(function, args=None, local=False, config=None)

Run salt module functions, literally so handy πŸ”₯

>>> host.salt("pkg.version", "nginx")
'1.6.2-5'
>>> host.salt("pkg.version", ["nginx", "php5-fpm"])
{'nginx': '1.6.2-5', 'php5-fpm': '5.6.7+dfsg-1'}
>>> host.salt("grains.item", ["osarch", "mem_total", "num_cpus"])
{'osarch': 'amd64', 'num_cpus': 4, 'mem_total': 15520}

Run salt-call sys.doc to get a complete list of functions πŸ‘‘


πŸŽ€ Service

class Service(name)

Test services 🌈

Implementations:

  • Linux: detect Systemd, Upstart or OpenRC, fallback to SysV
  • FreeBSD: service(1)
  • OpenBSD: /etc/rc.d/$name check for is_running rcctl ls on for is_enabled (only OpenBSD >= 5.8)
  • NetBSD: /etc/rc.d/$name onestatus for is_running (is_enabled is not yet implemented)

property exists

Test if service is exists πŸ’–

property is_running

Test if service is running πŸƒβ€β™€οΈ

property is_enabled

Test if service is enabled βœ…

property is_valid

Test if service is valid ✨

This method is only available in the systemd implementation β€” it will raise NotImplementedError in others implementation. 😬

property is_masked

Test if service is masked πŸ™ˆ

Same deal, bestie: this method is only available in the systemd implementation, and it will raise NotImplementedError in others implementations.

property systemd_properties

Properties of the service (unit). 🫢

Return service properties as a dict β€” empty properties are not returned.

>>> ntp = host.service("ntp")
>>> ntp.systemd_properties["FragmentPath"]
'/lib/systemd/system/ntp.service'
This method is only available in the systemd implementation, it will raise NotImplementedError in others implementations.

Note: based on systemctl show <https://man7.org/linux/man-pages/man1/systemctl.1.html>


πŸ¦‹ Socket

class Socket(socketspec)

Test listening tcp/udp and unix sockets πŸ‘‘

socketspec must be specified as <protocol>://<host>:<port>

This module requires the netstat command to on the target host.

Example:

  • Unix sockets: unix:///var/run/docker.sock
  • All ipv4 and ipv6 tcp sockets on port 22: tcp://22
  • All ipv4 sockets on port 22: tcp://0.0.0.0:22
  • All ipv6 sockets on port 22: tcp://:::22
  • udp socket on 127.0.0.1 port 69: udp://127.0.0.1:69

property is_listening

Test if socket is listening πŸ‘‚

>>> host.socket("unix:///var/run/docker.sock").is_listening
False
>>> # This HTTP server listen on all ipv4 addresses but not on ipv6
>>> host.socket("tcp://0.0.0.0:80").is_listening
True
>>> host.socket("tcp://:::80").is_listening
False
>>> host.socket("tcp://80").is_listening
False
Note: if you don't specify a host for udp and tcp sockets, then the socket is listening if and only if the socket listen on both all ipv4 and ipv6 addresses (ie 0.0.0.0 and ::). Lowkey important, don't skip this one πŸ’€

property clients: list[tuple[str, int] | None]

Return a list of clients connected to a listening socket 🫢

For tcp and udp sockets a list of pair (address, port) is returned. For unix sockets a list of None is returned (thus you can make a len() for counting clients).

>>> host.socket("tcp://22").clients
[('2001:db8:0:1', 44298), ('192.168.31.254', 34866)]
>>> host.socket("unix:///var/run/docker.sock")
[None, None, None]

classmethod get_listening_sockets()

Return a list of all listening sockets 🌟

>>> host.socket.get_listening_sockets()
['tcp://0.0.0.0:22', 'tcp://:::22', 'unix:///run/systemd/private', ...]

πŸ’œ Sudo

class Sudo(user=None)

Sudo module allow to run certain portion of code under another user. Main character energy fr fr πŸ‘‘

It is used as a context manager and can be nested. πŸͺ†

>>> Command.check_output("whoami")
'phil'
>>> with host.sudo():
...     host.check_output("whoami")
...     with host.sudo("www-data"):
...         host.check_output("whoami")
...
'root'
'www-data'

⭐ Supervisor

class Supervisor(name, supervisorctl_path='supervisorctl', supervisorctl_conf=None, _attrs_cache=None)

Test supervisor managed services πŸ’…

>>> gunicorn = host.supervisor("gunicorn")
>>> gunicorn.status
'RUNNING'
>>> gunicorn.is_running
True
>>> gunicorn.pid
4242

The path where supervisorctl and its configuration file reside can be specified.

>>> gunicorn = host.supervisor("gunicorn", "/usr/bin/supervisorctl", "/etc/supervisor/supervisord.conf")
>>> gunicorn.status
'RUNNING'

property is_running

Return True if managed service is in status RUNNING βœ…

property status

Return the status of the managed service 🎯

Status can be STOPPED, STARTING, RUNNING, BACKOFF, STOPPING, EXITED, FATAL, UNKNOWN.

See <http://supervisord.org/subprocess.html#process-states>

property pid

Return the pid (as int) of the managed service πŸ”’

classmethod get_services(supervisorctl_path='supervisorctl', supervisorctl_conf=None)

Get a list of services running under supervisor 🌈

>>> host.supervisor.get_services()
[<Supervisor(name="gunicorn", status="RUNNING", pid=4232)>
 <Supervisor(name="celery", status="FATAL", pid=None)>]

The path where supervisorctl and its configuration file reside can be specified.

>>> host.supervisor.get_services("/usr/bin/supervisorctl", "/etc/supervisor/supervisord.conf")
[<Supervisor(name="gunicorn", status="RUNNING", pid=4232)>
 <Supervisor(name="celery", status="FATAL", pid=None)>]

🧚 Sysctl

class Sysctl(name)

Test kernel parameters πŸŽ€

>>> host.sysctl("kernel.osrelease")
"3.16.0-4-amd64"
>>> host.sysctl("vm.dirty_ratio")
20

πŸ’– SystemInfo

class SystemInfo

Return system information ✨

property type

OS type πŸ–₯️

>>> host.system_info.type
'linux'

property distribution

Distribution name 🌸

>>> host.system_info.distribution
'debian'

property release

Distribution release number πŸ”’

>>> host.system_info.release
'10.2'

property codename

Release code name 🏷️

>>> host.system_info.codename
'bullseye'

property arch

Host architecture πŸ—οΈ

>>> host.system_info.arch
'x86_64'

🌟 User

class User(name=None)

Test unix users πŸ‘€

If name is not supplied, test the current user.

property name

Return user name πŸ’–

property exists

Test if user exists πŸ‘€

>>> host.user("root").exists
True
>>> host.user("nosuchuser").exists
False

property uid

Return user ID πŸ”’

property gid

Return effective group ID 🫢

property group

Return effective group name 🏷️

property gids

Return the list of user group IDs πŸ“‹

property groups

Return the list of user group names πŸ“‹

property home

Return the user home directory 🏠

property shell

Return the user login shell 🐚

property password

Return the encrypted user password πŸ”

property password_max_days

Return the maximum number of days between password changes πŸ“†

property password_min_days

Return the minimum number of days between password changes πŸ“†

property gecos

Return the user comment/gecos field πŸ’¬

property expiration_date β€” when does this account expire, bestie? That's what this gives you πŸŽ‚

>>> host.user("phil").expiration_date
datetime.datetime(2020, 1, 1, 0, 0)
>>> host.user("root").expiration_date
None

property get_all_users β€” hands you a list of every local and remote user name, the whole guest list πŸ’…

>>> host.user().get_all_users
["root", "bin", "daemon", "lp", <...>]

property get_local_users β€” same energy but local-only, no remote besties invited 🏠

>>> host.user().get_local_users
["root", "bin", "daemon", "lp", <...>]

CommandResult

class CommandResult(backend: BaseBackend, exit_status: int, command: bytes, _stdout: str | bytes, _stderr: str | bytes)

okay so this is the little object that holds all the deets from a command you ran β€” it's giving "receipts for everything" 🧾✨

Example, because example slays:

>>> cmd = host.run("ls -l /etc/passwd")
>>> cmd.rc
0
>>> cmd.stdout
'-rw-r--r-- 1 root root 1790 Feb 11 00:28 /etc/passwd\n'
>>> cmd.stderr
''
>>> cmd.succeeded
True
>>> cmd.failed
False

property succeeded: bool β€” was the command a win? πŸ’–

>>> host.run("true").succeeded
True

property failed: bool β€” was it a flop? πŸ’€

>>> host.run("false").failed
True

property rc: int β€” grabs the returncode of a command, the raw numeric tea β˜•

>>> host.run("true").rc
0

property stdout: str β€” gets the standard output (stdout) stream of an executed command, as a str πŸŽ€

>>> host.run("mkdir -v new_directory").stdout
mkdir: created directory 'new_directory'

property stderr: str β€” gets the standard error (stderr) stream of an executed command, as a str 😭

>>> host.run("mkdir new_directory").stderr
mkdir: cannot create directory 'new_directory': File exists

property stdout_bytes: bytes β€” same stdout but as bytes, for when you're feeling raw πŸ¦‹

>>> host.run("mkdir -v new_directory").stdout_bytes
b"mkdir: created directory 'new_directory'"

property stderr_bytes: bytes β€” same stderr but as bytes 😀

>>> host.run("mkdir new_directory").stderr_bytes
b"mkdir: cannot create directory 'new_directory': File exists"

✨ API ✨

🫢 Connection API 🫢

big news: you can use testinfra outside of pytest?? iconic. You can dynamically grab a host instance and then call functions or access members of the respective modules:

>>> import testinfra
>>> host = testinfra.get_host("paramiko://root@server:2222", sudo=True)
>>> host.file("/etc/shadow").mode == 0o640
True

For instance β€” and this is lowkey the coolest party trick β€” you could make a test that compares two files on two different servers:

import testinfra

def test_same_passwd():
    a = testinfra.get_host("ssh://a")
    b = testinfra.get_host("ssh://b")
    assert a.file("/etc/passwd").content == b.file("/etc/passwd").content

🌈 Examples 🌈

Parametrize your tests

pytest supports test parametrization ( https://pytest.org/latest/parametrize.html ) and you should absolutely use it, bestie:

# BAD: If the test fails on nginx, python is not tested
def test_packages(host):
    for name, version in (
        ("nginx", "1.6"),
        ("python", "2.7"),
    ):
        pkg = host.package(name)
        assert pkg.is_installed
        assert pkg.version.startswith(version)

# GOOD: Each package is tested
# $ pytest -v test.py
# [...]
# test.py::test_package[local-nginx-1.6] PASSED
# test.py::test_package[local-python-2.7] PASSED
# [...]
import pytest

@pytest.mark.parametrize("name,version", [
    ("nginx", "1.6"),
    ("python", "2.7"),
])
def test_packages(host, name, version):
    pkg = host.package(name)
    assert pkg.is_installed
    assert pkg.version.startswith(version)

see how the GOOD one tests each package? that's the vibe check passing βœ…

Using unittest

testinfra can also run with the standard Python unit test framework, unittest ( https://docs.python.org/3/library/unittest.html ), instead of pytest β€” yes, the classics still eat 🍴

import unittest
import testinfra

class Test(unittest.TestCase):

    def setUp(self):
        self.host = testinfra.get_host("paramiko://root@host")

    def test_nginx_config(self):
        self.assertEqual(self.host.run("nginx -t").rc, 0)

    def test_nginx_service(self):
        service = self.host.service("nginx")
        self.assertTrue(service.is_running)
        self.assertTrue(service.is_enabled)

if __name__ == "__main__":
    unittest.main()
$ python test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.705s

OK

Integration with Vagrant

Vagrant ( https://www.vagrantup.com/ ) is a tool to set up and provision development environments (virtual machines) πŸ–₯️

When your Vagrant machine is up and running, you can easily run your testinfra test suite on it:

vagrant ssh-config > .vagrant/ssh-config
pytest --hosts=default --ssh-config=.vagrant/ssh-config tests.py

Integration with Jenkins

Jenkins ( https://jenkins-ci.org/ ) is a well known open source continuous integration server πŸ‘‘

If your Jenkins slave can run Vagrant, your build scripts can be like:

pip install pytest-testinfra paramiko
vagrant up
vagrant ssh-config > .vagrant/ssh-config
pytest --hosts=default --ssh-config=.vagrant/ssh-config --junit-xml junit.xml tests.py

Then configure Jenkins to get test results from the junit.xml file. done and done πŸ’…

Integration with Nagios

your tests will usually be validating that the services you're deploying run correctly. This kind of test is close to monitoring checks, so let's push them to Nagios ( https://www.nagios.org/ ) ! 🚨

The testinfra option --nagios enables a behavior compatible with a nagios plugin:

$ pytest -qq --nagios --tb line test_ok.py; echo $?
TESTINFRA OK - 2 passed, 0 failed, 0 skipped in 2.30 seconds
..
0

$ pytest -qq --nagios --tb line test_fail.py; echo $?
TESTINFRA CRITICAL - 1 passed, 1 failed, 0 skipped in 2.24 seconds
.F
/usr/lib/python3/dist-packages/example/example.py:95: error: [Errno 111] error msg
2

You can run these tests from the nagios master or in the target host with NRPE ( https://en.wikipedia.org/wiki/Nagios#Nagios_Remote_Plugin_Executor ) 🧚

Integration with KitchenCI

KitchenCI (aka Test Kitchen) can use testinfra via its shell verifier. Add the following to your .kitchen.yml β€” and heads up, this requires installing paramiko additionally (on your host machine, not in the VM handled by kitchen) 🍳

verifier:
  name: shell
  command: pytest --hosts="paramiko://${KITCHEN_USERNAME}@${KITCHEN_HOSTNAME}:${KITCHEN_PORT}?ssh_identity_file=${KITCHEN_SSH_KEY}" --junit-xml "junit-${KITCHEN_INSTANCE}.xml" "test/integration/${KITCHEN_SUITE}"

Test Docker images

Docker is a handy way to test your infrastructure code 🐳 This recipe shows how to build and run Docker containers with Testinfra by overloading the host fixture:

import pytest
import subprocess
import testinfra

# scope='session' uses the same container for all the tests;
# scope='function' uses a new container per test function.
@pytest.fixture(scope='session')
def host(request):
    # build local ./Dockerfile
    subprocess.check_call(['docker', 'build', '-t', 'myimage', '.'])
    # run a container
    docker_id = subprocess.check_output(
        ['docker', 'run', '-d', 'myimage']).decode().strip()
    # return a testinfra connection to the container
    yield testinfra.get_host("docker://" + docker_id)
    # at the end of the test suite, destroy the container
    subprocess.check_call(['docker', 'rm', '-f', docker_id])

def test_myimage(host):
    # 'host' now binds to the container
    assert host.check_output('myapp -v') == 'Myapp 1.0'

πŸ’œ Support πŸ’œ

If you have questions or need help with testinfra, please consider one of the following 🫢

Issue Tracker

Check out existing issues on the project issue tracker ( https://github.com/pytest-dev/pytest-testinfra/issues ) πŸ›

IRC

You can also ask questions on IRC in #pytest ( https://web.libera.chat/?channel=#pytest ) channel on the libera.chat ( https://libera.chat/ ) network πŸ’¬

pytest documentation

testinfra is implemented as a pytest plugin, so to get the most out of it please read the pytest documentation ( https://docs.pytest.org/en/latest/ ) πŸ“š

Community Contributions


πŸ‘‘ Author πŸ‘‘

Philippe Pepiot

2025, Philippe Pepiot


and that's the whole page, bestie β€” no cap, you're now officially testinfra-literate πŸ’…βœ¨ this was TESTINFRA(1), version 10.2.2, dated Dec 20, 2025. go forth and test literally everything πŸ¦‹πŸ’–

πŸ“¦ where this page comes from