Modules¶
Testinfra modules are provided through the host fixture, declare it as arguments of your test function to make it available within it.
def test_foo(host):
# [...]
host¶
-
class
Host
(backend)¶ -
has_command_v
¶ Return True if command -v is available
-
exists
(command)¶ Return True if given command exist in $PATH
-
find_command
(command, extrapaths=('/sbin', '/usr/sbin'))¶ Return path of given command
raise ValueError if command cannot be found
-
run
(command, *args, **kwargs)¶ 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, command, *args, **kwargs)¶ Run command and check it return an expected exit status
Parameters: expected – A list of expected exit status Raises: AssertionError
-
run_test
(command, *args, **kwargs)¶ Run command and check it return an exit status of 0 or 1
Raises: AssertionError
-
check_output
(command, *args, **kwargs)¶ Get stdout of a command which has run successfully
Returns: stdout without trailing newline Raises: AssertionError
-
classmethod
get_host
(hostspec, **kwargs)¶ Return a Host instance from hostspec
hostspec should be like <backend_type>://<name>?param1=value1¶m2=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 is enabled by default, you can disable it with check=False.
Become is False by default. You can enable it with become=True.
Ansible arguments that are not related to the Ansible inventory or connection (both managed by testinfra) are also accepted through 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)¶ Exception raised when an error occur in an ansible call
result from ansible can be accessed through the
result
attribute>>> 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
Can also be use within a network namespace.
>>> localhost = host.addr("localhost", "ns1") >>> localhost.is_resolvable True
Network namespaces can only be used if ip command is available because in this case, the module use ip-netns as command prefix. In the other case, it will raise NotImplementedError.
-
name
¶ Return host name
-
namespace
¶ Return network namespace
-
namespace_exists
¶ Test if the network namespace exists
-
is_resolvable
¶ Return if address is resolvable
-
is_reachable
¶ Return if address is reachable
-
ip_addresses
¶ Return IP addresses of host
-
ipv4_addresses
¶ Return IPv4 addresses of host
-
ipv6_addresses
¶ Return IPv6 addresses of host
-
port
(port)¶ Return address-port pair
-
BlockDevice¶
-
class
BlockDevice
(name)¶ Information for block device.
Should be used with sudo or under root.
If device is not a block device, RuntimeError is raised.
-
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
-
size
¶ Return size if the device in bytes.
>>> host.block_device("/dev/sda1").size 512110190592
-
sector_size
¶ Return sector size for the device in bytes.
>>> host.block_device("/dev/sda1").sector_size 512
-
block_size
¶ Return block size for the device in bytes.
>>> host.block_device("/dev/sda").block_size 4096
-
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
-
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
-
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 return list of all containers, including non-running containers.
Filtering can be done using filters keys defined on https://docs.docker.com/engine/reference/commandline/ps/#filtering
Multiple filters for a given key is handled by giving a list of string as 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>]
-
classmethod
File¶
-
class
File
(path)¶ Test various files attributes
-
exists
¶ Test if file exists
>>> host.file("/etc/passwd").exists True >>> host.file("/nonexistent").exists False
-
is_file
¶ Test if the path is a regular file
-
is_directory
¶ Test if the path exists and a directory
-
is_executable
¶ Test if the path exists and permission to execute is granted
-
is_pipe
¶ Test if the path exists and is a pipe
-
is_socket
¶ Test if the path exists and is a socket
-
is_symlink
¶ Test if the path exists and is a symbolic link
-
linked_to
¶ Resolve symlink
>>> host.file("/var/lock").linked_to '/run/lock'
-
user
¶ Return file owner as string
>>> host.file("/etc/passwd").user 'root'
-
uid
¶ Return file user id as integer
>>> host.file("/etc/passwd").uid 0
-
group
¶ Return file group name as string
-
gid
¶ Return file group id as integer
-
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.
>>> 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.
-
md5sum
¶ Compute the MD5 message digest of the file content
-
sha256sum
¶ Compute the SHA256 message digest of the file content
-
content
¶ Return file content as bytes
>>> host.file("/tmp/foo").content b'caf\xc3\xa9'
-
content_string
¶ Return file content as string
>>> host.file("/tmp/foo").content_string 'café'
-
mtime
¶ Return time of last modification as datetime.datetime object
>>> host.file("/etc/passwd").mtime datetime.datetime(2015, 3, 15, 20, 25, 40)
-
size
¶ Return size of file in bytes
-
listdir
()¶ Return list of items under the directory
>>> host.file("/tmp").listdir() ['foo_file', 'bar_dir']
-
Group¶
Interface¶
-
class
Interface
(name, family=None)¶ Test network interfaces
>>> host.interface("eth0").exists True
Optionally, the protocol family to use can be enforced.
>>> host.interface("eth0", "inet6").addresses ['fe80::e291:f5ff:fe98:6b8c']
-
exists
¶
-
speed
¶
-
addresses
¶ Return ipv4 and ipv6 addresses on the interface
>>> host.interface("eth0").addresses ['192.168.31.254', '192.168.31.252', 'fe80::e291:f5ff:fe98:6b8c']
-
routes
(scope=None)¶ Return the routes associated with the interface, optionally filtered by scope (“host”, “link” or “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
()¶ Return the names of all the interfaces.
>>> host.interface.names() ['lo', 'tunl0', 'ip6tnl0', 'eth0']
-
classmethod
default
(family=None)¶ Return the interface used for the default route.
>>> host.interface.default() <interface eth0>
Optionally, the protocol family to use can be enforced.
>>> host.interface.default("inet6") None
-
Iptables¶
-
class
Iptables
¶ Test iptables rule exists
-
rules
(table='filter', chain=None, version=4)¶ Returns list of iptables rules
Based on output of iptables -t TABLE -S CHAIN command
- optionally takes 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)¶ Test Mount Points
-
exists
¶ Return True if the mountpoint exists
>>> host.mount_point("/").exists True
>>> host.mount_point("/not/a/mountpoint").exists False
-
filesystem
¶ Returns the filesystem type associated
>>> host.mount_point("/").filesystem 'ext4'
-
device
¶ Return the device associated
>>> host.mount_point("/").device '/dev/sda1'
-
options
¶ Return 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
>>> 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)¶ Test packages status and version
-
is_installed
¶ Test if the package is installed
>>> host.package("nginx").is_installed True
Supported package systems:
- apk (Alpine)
- apt (Debian, Ubuntu, …)
- pacman (Arch, Manjaro )
- pkg (FreeBSD)
- pkg_info (NetBSD)
- pkg_info (OpenBSD)
- rpm (RHEL, RockyLinux, Fedora, …)
-
release
¶ Return the release specific info from the package version
>>> host.package("nginx").release '1.el6'
-
version
¶ Return 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')¶ Test pip package manager and packages
-
is_installed
¶ Test if the package is installed
>>> host.package("pip").is_installed True
-
version
¶ Return package version as returned by pip
>>> host.package("pip").version '18.1'
-
classmethod
check
(pip_path='pip')¶ Verify installed packages have compatible dependencies.
>>> cmd = host.pip_package.check() >>> cmd.rc 0 >>> cmd.stdout No broken requirements found.
Can only be used if pip check command is available, for pip versions >= 9.0.0.
-
classmethod
get_packages
(pip_path='pip')¶ Get all installed packages and versions returned by pip list:
>>> host.pip_package.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')¶ Get all outdated packages with current and latest version
>>> host.pip_package.get_outdated_packages( ... pip_path='~/venv/website/bin/pip') {'Django': {'current': '1.10.2', 'latest': '1.10.3'}}
-
Podman¶
-
class
Podman
(name)¶ Test podman containers running on system.
Example:
>>> nginx = host.podman("app_nginx") >>> nginx.is_running True >>> nginx.id '7e67dc7495ca8f451d346b775890bdc0fb561ecdc97b68fb59ff2f77b509a8fe' >>> nginx.name 'app_nginx'
-
classmethod
get_containers
(**filters)¶ Return a list of containers
By default return list of all containers, including non-running containers.
Filtering can be done using filters keys defined in podman-ps(1).
Multiple filters for a given key is handled by giving a list of string as value.
>>> 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>]
-
classmethod
Process¶
-
class
Process
¶ Test Processes attributes
Processes are selected using
filter()
orget()
, attributes names are described in the ps(1) man page.>>> 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)¶ Get a list of matching process
>>> host.process.filter(user="root", comm="zsh") [<process zsh (pid=2715)>, <process zsh (pid=10502)>, ...]
-
get
(**filters)¶ Get one matching process
Raise
RuntimeError
if no process found or multiple process matching filters.
-
PuppetResource¶
-
class
PuppetResource
(type, name=None)¶ Get puppet resources
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¶
Salt¶
-
class
Salt
(function, args=None, local=False, config=None)¶ Run salt module functions
>>> 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
foris_running
rcctl ls on
foris_enabled
(only OpenBSD >= 5.8) - NetBSD:
/etc/rc.d/$name onestatus
foris_running
(is_enabled
is not yet implemented)
-
is_running
¶ Test if service is running
-
is_enabled
¶ Test if service is enabled
-
is_valid
¶ Test if service is valid
This method is only available in the systemd implementation, it will raise
NotImplementedError
in others implementation
-
is_masked
¶ Test if service is masked
This method is only available in the systemd implementation, it will raise
NotImplementedError
in others implementations
-
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 implementationsNote: based on systemctl show
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
-
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 ::)
-
clients
¶ 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', ...]
- Unix sockets:
Sudo¶
-
class
Sudo
(user=None)¶ Sudo module allow to run certain portion of code under another user.
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'
-
is_running
¶ Return True if managed service is in status RUNNING
-
status
¶ Return the status of the managed service
Status can be STOPPED, STARTING, RUNNING, BACKOFF, STOPPING, EXITED, FATAL, UNKNOWN.
-
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
-
type
¶ OS type
>>> host.system_info.type 'linux'
-
distribution
¶ Distribution name
>>> host.system_info.distribution 'debian'
-
release
¶ Distribution release number
>>> host.system_info.release '10.2'
-
codename
¶ Release code name
>>> host.system_info.codename 'bullseye'
-
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
-
name
¶ Return user name
-
exists
¶ Test if user exists
>>> host.user("root").exists True >>> host.user("nosuchuser").exists False
-
uid
¶ Return user ID
-
gid
¶ Return effective group ID
-
group
¶ Return effective group name
-
gids
¶ Return the list of user group IDs
-
groups
¶ Return the list of user group names
-
home
¶ Return the user home directory
-
shell
¶ Return the user login shell
-
password
¶ Return the encrypted user password
-
password_max_days
¶ Return the maximum number of days between password changes
-
password_min_days
¶ Return the minimum number of days between password changes
-
gecos
¶ Return the user comment/gecos field
-
expiration_date
¶ Return the account expiration date
>>> host.user("phil").expiration_date datetime.datetime(2020, 1, 1, 0, 0) >>> host.user("root").expiration_date None
-