TESTINFRA(1) testinfra TESTINFRA(1) (NAME) testinfra - testinfra : (ABOUT) Testinfra (unit tests) Python Salt Ansible Puppet Chef . Testinfra Serverspec (plugin) Pytest . (LICENSE) Apache License 2.0 Creative Commons NoDerivatives 4.0 License . . (QUICK START) testinfra pip: $ pip install pytest-testinfra # or install the devel version $ pip install 'git+https://github.com/pytest-dev/pytest-testinfra@main#egg=pytest-testinfra' 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 : $ 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 o [FIX] sdist 10.2.1 o [FIX] pypi o [] o [] Paramiko 2MB stdout o [] shell ansible o [] 4 "systemctl is-active" o [] KeyError MountPoint.__repr__() mount o [] pytest py.test o [] backend (host spec) o [] lint py 3.12 3.13 o [] hatchling o [] "args" run_winrm() o [] ruff format black/isort o [] ruff flake8 o [] f-string str.format() o [] dict list tuple (typing) o [] python 3.11 o [] salt o [] o [] crypt 10.1.1 o [] ssh control path o [] is_installed rpm rpm o [] service.exists 10.1.0 o [] Interface.link o [] (symlinks) o [] pytest>=6 (future annotations) pytest<7 10.0.0 o [] Ansible: get_variables() o [] testinfra/modules/blockdevice: stderr o [] CommandResult o [] systemd o [] Environment o [] (types) plugin.py o [] RHEL package o [] brew package o [] Service.exists o [] CommandResult dataclass 9.0.0 o [ ] pytest-testinfra python >= 3.9 o [ ] PipPackage o [] ControlPath SSH (#713) o [] SSH ConnectionResetError (#708) o [] openSUSE Leap Tumbleweed rpm o [] group 8.1.0 o [] Windows File Service o [] File.is_executable 8.0.0 o [] Group.members o [] File.inode o [] Interface.routes() o [] Docker.is_restarting o [] Interface.default() o [] busybox Process o [] KeyError SysInfo o [ ] python 3.7 o [] command -v dash 7.0.0 o [] ssh Paramiko o [] chroot o [] Manjaro-Linux o [] Cloudlinux o [ ] python 3.6 (EOL) 6.8.0 o [] AlmaLinux RockyLinux 6.7.0 o [] ansible_connection community.docker.docker o [] ssh_extra_args 6.6.0 o [] o [] ANSIBLE_SSH_COMMON_ARGS ANSIBLE_SSH_EXTRA_ARGS ansible o [] salt o [] AttributeError "command" "which" 6.5.0 o which "command -v" o realpath "readlink -f" o ansible: o package RpmPackage Fedora o supervisor != 0 o ansible ascii o python3 python 6.4.0 o Interface (#615) o Service.systemd_properties (#612) 6.3.0 o #451 pytest -p no:terminal o client_version() server_version() version() docker. 6.2.0 o #590: Windows Systeminfo (#592) o addr (#596) o pip check PipPackage (#605) o pip: installed version (#606) o supervisorctl supervisord.conf (#536) 6.1.0 o package CentOS dpkg (#575) #570 o get_backends() (#572) o /run/systemd/system/ systemd ( #546) o ssh_args ansible.cfg o python >= 3.6 o ValueError python 3.8+ --nagios. 6.0.0 o : testinfra . PyPi pytest-testinfra . testinfra requirements pytest-testinfra . 5.3.1 o is_masked systemd 5.3.0 o is_masked systemd 5.2.2 o iptables: -w iptables pytest-xdist. o 5.2.0 o kubeconfig kubernetes o file.__ne__ python >= 3.5 5.1.0 o remote_user remote_port ansible.cfg o arch () system_info 5.0.0 o (Breaking change): host.file().listdir() 4.1.0 o ansible host.ansible() o host.file.listdir() . 4.0.0 o python2 3.4.0 o podman o : testinfra python2 python3 . 3.3.0 o extras (#454) o kitchen o ansible "password" ansible o "openshift" 3.2.1 o Process ( ...) #505 3.2.0 o "environment" o "block_device" o --force-ansible o ansible o (escape) ssh ansible 3.1.0 o ssh (persistent) . controlpersist=0 . o ssh ansible paramiko ssh . ansible_ssh_common_args ansible_ssh_extra_args . o ansible "force_ansible" True testinfra ansible . o ansible force_ansible=True testinfra ( "network_cli"). 3.0.6 o DEBUG INFO. o ansible #470 o kubeconfig kubectl #460 o ProxyCommand ssh_config paramiko 3.0.5 o (timeout) ssh/paramiko o ansible_private_key_file ansible 3.0.4 o lxc lxd ansible 3.0.3 o RequestTTY paramiko ssh o "groups" ansible.get_variables() 2.X testinfra o ansible inventory "localhost" o ansible 2.8 inventory o ansible/paramiko ~/.ssh/config o --ssh-config --ssh-identity-file ansible 3.0.1 o ipv6 backend paramiko ssh ansible. o --connection=ansible 3.0.0 o backend ansible ansible 2.8 (license) . . ansible backend testinfra local ssh docker . o Windows . "package" Chocolatey "user" . 2.1.0 o docker: (classmethod) get_containers() o socket: ipv6 ss o service: systemd sysv "systemctl is-active" 2.0.0 o addr o "testinfra" "py.test" o fixture fixture "host" . o ansible <= 2.4 1.19.0 o docker o pytest 4 1.18.0 o (urlencode) "user:pass@host" (#387) o (double logging) pytest testinfra o python 2.6 o (timeout) backend winrm 1.17.0 o "become" ansible o failed/succeeded run() 1.16.0 o packaging: setuptools_scm pbr o iptables: ip6tables o sysctl: sysctl PATH (/sbin) 1.15.0 o ss netstat "sbin" Centos (359) o (workaround) o "starting" Service Alpine linux o no_ssl no_verify_ssl backend WinRM 1.14.1 o (#347) 1.13.1 o Socket OpenBSD (#338) 1.14.0 o backend lxc o Socket: is_listening unix o namespace container backend kubernetes o (cache) inventory ansible backend ansible o Service: Centos 6 o File: (string paths) 1.13.1 o package: is_installed version (#321 #326) o ansible: pytest-xdist (#316) o socket: udp (#311) o ssh backend: GSSAPI 1.12.0 o ansible: ansible 2.5 o pip: pip 10 (#299) 1.11.1 o Socket: ss --no-header (#293) 1.11.0 o ansible ansible backend (#288) o Socket: ss netstat (#124) o service process systeminfo Alpine (#283) 1.10.1 o ()get_variables ansible>=2.0,<2.4 (#274) o Paramiko: RequireTTY SSHConfig (#247) 1.10.0 o iptables 1.9.1 o testinfra doctest (#268) o Service: ()is_valid systemd o ()file.linked_to Mac OS 1.9.0 o Interface: 'ip' PATH o --nagios python 3 1.8.0 o testinfra ( 2.0 ) py.test #135 o --nagios py.test 1.7.1 o ansible 2.4 (#249) 1.7.0 o Salt: (#230) o WinRM backend o Socket: ipv4 ipv6 (#234) o Service: upstart (#243) 1.6.5 o Service: ()is_enabled OpenBSD o ssh backend paramiko ssh o (~) ssh-config ssh-identity-file ansible-inventory 1.6.4 o Service: 'service' $PATH #211 o 1.6.3 o pytest 3.1 #204 1.6.2 o wheel 1.6.1 1.6.1 o ansible 2.3 python 3 (#197) 1.6.0 o 'host' . ( 2.0 ). 1.5.5 o backends: ansible backend ansible >= 2.3 (#195) o backends: UTF-8 ASCII . o Service: is_running() Upstart. 1.5.3 o Sudo: backend . 1.5.2 o become_user ansible. 1.5.1 o importlib python 2.6. 1.5.0 o kubectl. o Command: carriage return check_output (#164). o Package: version() release() rpm. o User: gecos () (#155). 1.4.5 o SystemInfo: (codename) VERSION_CODENAME /etc/os-release ( lsb_release). o Package: release rpm. (Invocation) (Test multiple hosts) Testinfra paramiko ( ssh ) : $ 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 ==================== : testinfra_hosts = ["localhost", "root@webserver:2222"] def test_foo(host): [....] (Parallel execution) pytest-xdist : $ 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 Pytest . (Connection backends) Testinfra . extras ( ). : $ pip install pytest-testinfra[ansible,salt] --sudo (superuser) --sudo-user ..SS " (General Host specification)" --hosts Testinfra . (comma) . user password port . . (Syntax): --hosts=://:@: : o : ( ssh docker paramiko local) o : () o : () o : IP o : () () ( ":") user password RFC 3986 (percent-encoded) . Python urllib.parse.quote() . : 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) . : SSH : testinfra --hosts=ssh://user:password@hostname:22 Docker: testinfra --hosts=docker://container_id : testinfra --hosts=ssh://user:password@hostname:22,docker://container_id,local:// local ( --hosts ). (subprocess) : $ pytest --sudo test_myinfra.py paramiko . Paramiko SSHv2 Python . Testinfra ( ssh-agent) . ssh-config : $ pytest --ssh-config=/path/to/ssh_config --hosts=server docker Docker Docker . docker exec : $ pytest --hosts='docker://[user@]container_id_or_name' Test Docker images <#test-docker- images> ..SS podman backend Podman Podman . backend podman exec : $ pytest --hosts='podman://[user@]container_id_or_name' ssh backend SSH ssh . : $ 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' (timeout) ControlPersist . controlpersist=0 . salt backend salt salt salt-master : $ pytest --hosts='salt://*' $ pytest --hosts='salt://minion1,salt://minion2' $ pytest --hosts='salt://web*' $ pytest --hosts='salt://G@os:Debian' Testinfra salt . glob . ansible (inventories) Testinfra backend Testinfra . backend ansible:// --hosts : $ pytest --hosts='ansible://all' # tests all inventory hosts $ pytest --hosts='ansible://host1,ansible://host2' $ pytest --hosts='ansible://web*' inventory --ansible-inventory (/etc/ansible/hosts) . ansible_connection inventory backend : local ssh paramiko docker. ( --force-ansible ) testinfra Ansible backend : $ pytest --force-ansible --hosts='ansible://all' $ pytest --hosts='ansible://host?force_ansible=True' backend Ansible ansible_ssh_private_key_file ansible_private_key_file ansible_user ansible_ssh_pass ( ) SSH . --ssh-identity-file --ssh-config . ansible ( Ansible ): o ANSIBLE_REMOTE_USER o ANSIBLE_SSH_EXTRA_ARGS o ANSIBLE_SSH_COMMON_ARGS o ANSIBLE_REMOTE_PORT o ANSIBLE_BECOME_USER o ANSIBLE_BECOME kubectl backend kubectl Kubernetes . backend kubectl exec pod 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 OpenShift . oc exec pod 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 winrm 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 read_timeout_sec operation_timeout_sec : $ pytest --hosts='winrm://vagrant@127.0.0.1:2200?read_timeout_sec=120&operation_timeout_sec=100' LXC/LXD LXC LXC LXD . lxc exec : $ pytest --hosts='lxc://container_name' (Modules) Testinfra host . def test_foo(host): # [...] host class Host(backend: BaseBackend) ansible testinfra.modules.ansible.Ansible addr testinfra.modules.addr.Addr blockdevice testinfra.modules.blockdevice.BlockDevice docker testinfra.modules.docker.Docker environment testinfra.modules.environment.Environment file testinfra.modules.file.File group testinfra.modules.group.Group interface testinfra.modules.interface.Interface iptables testinfra.modules.iptables.Iptables mount_point testinfra.modules.mountpoint.MountPoint package testinfra.modules.package.Package pip testinfra.modules.pip.Pip podman testinfra.modules.podman.Podman process testinfra.modules.process.Process puppet_resource testinfra.modules.puppet.PuppetResource facter testinfra.modules.puppet.Facter salt testinfra.modules.salt.Salt service testinfra.modules.service.Service socket testinfra.modules.socket.Socket sudo testinfra.modules.sudo.Sudo supervisor testinfra.modules.supervisor.Supervisor sysctl testinfra.modules.sysctl.Sysctl system_info testinfra.modules.systeminfo.SystemInfo user testinfra.modules.user.User property has_command_v: bool command -v True exists(command: str) -> bool $PATH True . find_command(command: str, extrapaths: Iterable[str] = ('/sbin', '/usr/sbin')) -> str ValueError run(command: str, *args: str, **kwargs: Any) -> CommandResult <# testinfra.backend.base.CommandResult> rc ( ) stdout 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 : (shell quoting) (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 <#testinfra.backend.base .CommandResult> expected -- AssertionError run_test(command: str, *args: str, **kwargs: Any) -> CommandResult <#testinfra.backend.base.CommandResult> 0 1 AssertionError check_output(command: str, *args: str, **kwargs: Any) -> str stdout stdout AssertionError classmethod get_host(hostspec: str, **kwargs: Any) -> Host <# testinfra.host.Host> Host hostspec hostspec : ://?param1=value1¶m2=value2 **kwargs ( get_host("local://", sudo=True) get_host("local://?sudo=true") ) : >>> 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) Ansible ansible <#ansible-connection-backend> . (Check mode) check=False . Become False . become=True . Ansible inventory Ansible ( testinfra ) : o become_method str sudo su doas . o become_user str . o diff bool: () . o extra_vars dict JSON Ansible . o one_line bool: . o user str . o verbose int (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) ansible ansible result >>> 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() dict ansible >>> host.ansible.get_variables() { 'inventory_hostname': 'localhost', 'group_names': ['ungrouped'], 'foo': 'bar', } class Addr(name) : >>> 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 . >>> localhost = host.addr("localhost", "ns1") >>> localhost.is_resolvable True ip ip-netns . NotImplementedError . property name property namespace property namespace_exists property is_resolvable (resolve) property is_reachable property ip_addresses IP property ipv4_addresses IPv4 property ipv6_addresses IPv6 port(port) - BlockDevice class BlockDevice(name) . sudo (root) . RuntimeError . property is_partition True . >>> host.block_device("/dev/sda1").is_partition True >>> host.block_device("/dev/sda").is_partition False property size . >>> host.block_device("/dev/sda1").size 512110190592 property sector_size . >>> host.block_device("/dev/sda1").sector_size 512 property block_size . >>> host.block_device("/dev/sda").block_size 4096 property start_sector . . >>> host.block_device("/dev/sda1").start_sector 2048 >>> host.block_device("/dev/md0").start_sector 0 property is_writable ( RO ) True . >>> host.block_device("/dev/sda").is_writable True >>> host.block_device("/dev/loop1").is_writable False property ra Read Ahead . >>> host.block_device("/dev/sda").ra 256 class Docker(name) docker . : >>> nginx = host.docker("app_nginx") >>> nginx.is_running True >>> nginx.id '7e67dc7495ca8f451d346b775890bdc0fb561ecdc97b68fb59ff2f77b509a8fe' >>> nginx.name 'app_nginx' classmethod client_version() Docker classmethod server_version() Docker classmethod version(format=None) Docker (Go template). >>> host.docker.version() Client: Docker Engine - Community ... >>> host.docker.version("{{.Client.Context}}")) default classmethod get_containers(**filters) . . . >>> host.docker.get_containers() [, , ] # Get all running containers >>> host.docker.get_containers(status="running") [] # Get containers named "nginx" >>> host.docker.get_containers(name="nginx") [] # Get containers named "nginx" or "redis" >>> host.docker.get_containers(name=["nginx", "redis"]) [, ] Environment class Environment(name) : >>> host.environment() { "EDITOR": "vim", "SHELL": "/bin/bash", [...] } File class File(path) property exists >>> host.file("/etc/passwd").exists True >>> host.file("/nonexistent").exists False property is_file (regular file) property is_directory property is_executable property is_pipe (pipe) property is_socket (socket) property is_symlink (symlink) property linked_to >>> host.file("/var/lock").linked_to '/run/lock' property user >>> host.file("/etc/passwd").user 'root' property uid (UID) >>> host.file("/etc/passwd").uid 0 property group property gid (GID) property mode (file mode) (octal) >>> host.file("/etc/shadow").mode 416 # Oo640 octal >>> host.file("/etc/shadow").mode == 0o640 True >>> oct(host.file("/etc/shadow").mode) == '0o640' True stat . >>> import stat >>> host.file("/etc/shadow").mode == stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP True contains(pattern) (pattern) grep (regex) grep . property md5sum MD5 property sha256sum SHA256 property content >>> host.file("/tmp/foo").content b'caf\xc3\xa9' property content_string >>> host.file("/tmp/foo").content_string 'cafe' property mtime datetime.datetime >>> host.file("/etc/passwd").mtime datetime.datetime(2015, 3, 15, 20, 25, 40) property size listdir() >>> host.file("/tmp").listdir() ['foo_file', 'bar_dir'] Group class Group(name) property exists >>> host.group("wheel").exists True >>> host.group("nosuchgroup").exists False property get_all_groups >>> host.group("anyname").get_all_groups ["root", "wheel", "man", "tty", <...>] property get_local_groups >>> host.group("anyname").get_local_groups ["root", "wheel", "man", "tty", <...>] property gid property members . Interface class Interface(name, family=None) >>> host.interface("eth0").exists True . >>> host.interface("eth0", "inet6").addresses ['fe80::e291:f5ff:fe98:6b8c'] property exists property speed property addresses ipv4 ipv6 >>> host.interface("eth0").addresses ['192.168.31.254', '192.168.31.252', 'fe80::e291:f5ff:fe98:6b8c'] property link (link) . >>> 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) scope ("host" "link" "global"). >>> 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() . >>> host.interface.names() ['lo', 'tunl0', 'ip6tnl0', 'eth0'] classmethod default(family=None) (default route). >>> host.interface.default() . >>> host.interface.default("inet6") None class Iptables iptables rules(table='filter', chain=None, version=4) iptables iptables -t TABLE -S CHAIN : o table: filter o chain: o version: 4 (iptables) 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) (Mount Points) property exists True >>> host.mount_point("/").exists True >>> host.mount_point("/not/a/mountpoint").exists False property filesystem >>> host.mount_point("/").filesystem 'ext4' property device >>> host.mount_point("/").device '/dev/sda1' property options >>> host.mount_point("/").options ['rw', 'relatime', 'data=ordered'] classmethod get_mountpoints() MountPoint >>> host.mount_point.get_mountpoints() [, ] Package class Package(name) property is_installed >>> host.package("nginx").is_installed True : o apk (Alpine) o apt (Debian, Ubuntu, ...) o brew (macOS) o pacman (Arch, Manjaro ) o pkg (FreeBSD) o pkg_info (NetBSD) o pkg_info (OpenBSD) o rpm (RHEL, RockyLinux, Fedora, ...) property release (release) >>> host.package("nginx").release '1.el6' property version >>> host.package("nginx").version '1.2.1-2.2+wheezy3' class Pip(name, pip_path='pip') pip property is_installed >>> host.package("pip").is_installed True property version pip >>> host.package("pip").version '18.1' classmethod check(pip_path='pip') . >>> cmd = host.pip.check() >>> cmd.rc 0 >>> cmd.stdout No broken requirements found. pip check pip >= 9.0.0 . classmethod get_packages(pip_path='pip') 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') >>> host.pip.get_outdated_packages( ... pip_path='~/venv/website/bin/pip') {'Django': {'current': '1.10.2', 'latest': '1.10.3'}} Podman class Podman(name) podman . : >>> nginx = host.podman("app_nginx") >>> nginx.is_running True >>> nginx.id '7e67dc7495ca8f451d346b775890bdc0fb561ecdc97b68fb59ff2f77b509a8fe' >>> nginx.name 'app_nginx' classmethod get_containers(**filters) . podman-ps(1) . . >>> host.podman.get_containers() [, , ] # Get all running containers >>> host.podman.get_containers(status="running") [] # Get containers named "nginx" >>> host.podman.get_containers(name="nginx") [] # Get containers named "nginx" or "redis" >>> host.podman.get_containers(name=["nginx", "redis"]) [, ] Process class Process filter() get() ps(1) : . >>> 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) >>> host.process.filter(user="root", comm="zsh") [, , ...] get(**filters) RuntimeError . class PuppetResource(type, name=None) puppet puppet resource --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) (facts) 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) salt >>> 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} salt-call sys.doc Service class Service(name) : o : Systemd Upstart OpenRC SysV o : service(1) o : /etc/rc.d/$name check is_running rcctl ls on is_enabled ( OpenBSD >= 5.8) o : /etc/rc.d/$name onestatus is_running (is_enabled ) property exists property is_running property is_enabled (enabled) property is_valid systemd NotImplementedError property is_masked (masked) systemd NotImplementedError property systemd_properties (unit). dict . >>> ntp = host.service("ntp") >>> ntp.systemd_properties["FragmentPath"] '/lib/systemd/system/ntp.service' systemd NotImplementedError : systemctl show class Socket(socketspec) tcp/udp unix socketspec ://: netstat . : o Unix: unix:///var/run/docker.sock o tcp ipv4 ipv6 22: tcp://22 o ipv4 22: tcp://0.0.0.0:22 o ipv6 22: tcp://:::22 o udp 127.0.0.1 69: udp://127.0.0.1:69 property 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 : udp tcp ipv4 ipv6 ( 0.0.0.0 ::) property clients: list[tuple[str, int] | None] tcp udp ( ) . unix None ( ()len ). >>> 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() >>> host.socket.get_listening_sockets() ['tcp://0.0.0.0:22', 'tcp://:::22', 'unix:///run/systemd/private', ...] Sudo class Sudo(user=None) Sudo . (context manager) . >>> 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) supervisor >>> gunicorn = host.supervisor("gunicorn") >>> gunicorn.status 'RUNNING' >>> gunicorn.is_running True >>> gunicorn.pid 4242 supervisorctl . >>> gunicorn = host.supervisor("gunicorn", "/usr/bin/supervisorctl", "/etc/supervisor/supervisord.conf") >>> gunicorn.status 'RUNNING' property is_running RUNNING True property status STOPPED STARTING RUNNING BACKOFF STOPPING EXITED FATAL UNKNOWN . : property pid (pid int) classmethod get_services(supervisorctl_path='supervisorctl', supervisorctl_conf=None) supervisor >>> host.supervisor.get_services() [ ] supervisorctl . >>> host.supervisor.get_services("/usr/bin/supervisorctl", "/etc/supervisor/supervisord.conf") [ ] class Sysctl(name) >>> host.sysctl("kernel.osrelease") "3.16.0-4-amd64" >>> host.sysctl("vm.dirty_ratio") 20 SystemInfo class SystemInfo property type >>> host.system_info.type 'linux' property distribution >>> host.system_info.distribution 'debian' property release >>> host.system_info.release '10.2' property codename >>> host.system_info.codename 'bullseye' property arch >>> host.system_info.arch 'x86_64' User class User(name=None) name property name property exists >>> host.user("root").exists True >>> host.user("nosuchuser").exists False property uid property gid property group property gids property groups property home property shell property password property password_max_days property password_min_days property gecos /gecos property expiration_date >>> host.user("phil").expiration_date datetime.datetime(2020, 1, 1, 0, 0) >>> host.user("root").expiration_date None property get_all_users >>> host.user().get_all_users ["root", "bin", "daemon", "lp", <...>] property get_local_users >>> host.user().get_local_users ["root", "bin", "daemon", "lp", <...>] class CommandResult(backend: BaseBackend, exit_status: int, command: bytes, _stdout: str | bytes, _stderr: str | bytes) . : >>> 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 . >>> host.run("true").succeeded True property failed: bool . >>> host.run("false").failed True property rc: int (returncode) . >>> host.run("true").rc 0 property stdout: str (stdout) . >>> host.run("mkdir -v new_directory").stdout mkdir: created directory 'new_directory' property stderr: str (stderr) . >>> host.run("mkdir new_directory").stderr mkdir: cannot create directory 'new_directory': File exists property stdout_bytes: bytes (stdout) . >>> host.run("mkdir -v new_directory").stdout_bytes b"mkdir: created directory 'new_directory'" property stderr_bytes: bytes (stderr) . >>> host.run("mkdir new_directory").stderr_bytes b"mkdir: cannot create directory 'new_directory': File exists" API Connection API testinfra pytest . host : >>> import testinfra >>> host = testinfra.get_host("paramiko://root@server:2222", sudo=True) >>> host.file("/etc/shadow").mode == 0o640 True : 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 Pytest : # 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) unittest Testinfra pytest unittest : 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 Vagrant Vagrant ( ) . Vagrant testinfra : vagrant ssh-config > .vagrant/ssh-config pytest --hosts=default --ssh-config=.vagrant/ssh-config tests.py Jenkins Jenkins . (slave) Vagrant : 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 Jenkins junit.xml . Nagios . () Nagios ! --nagios Testinfra nagios : $ 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 nagios NRPE ..SS KitchenCI KitchenCI ( Test Kitchen) (verifier) shell testinfra . .kitchen.yml paramiko ( 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}" Docker (Docker) . Docker Testinfra (overloading) host . 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' testinfra : : (IRC) IRC #pytest [libera.chat]() . pytest testinfra pytest pytest : o Molecule Ansible Testinfra . (AUTHORS) Philippe Pepiot (COPYRIGHT) 2025, Philippe Pepiot 10.2.2 December 20, 2025 TESTINFRA(1)