Testinfra: Practical, Pytest-Friendly Infrastructure Testing
Modern infrastructure management requires consistency in the state of servers, and it's as crucial as the consistency of the code that runs on them. This is where the principles of Test-Driven Infrastructure (TDI) come into play, and Testinfra stands out as a powerful and elegant tool for implementing this practice. Testinfra is a Python framework that allows you to write unit tests for your infrastructure, verifying that your servers are configured exactly as you expect.
If you have used pytest to test your Python code, Testinfra will feel right at home. Testinfra is a pytest plugin, which means it leverages the power and flexibility of this popular Python testing framework. It allows you to write clean, readable tests to verify the state of various server components, such as packages, services, files, network sockets, and more. Think of it as unit testing for your servers. Instead of testing a function or a class, you're testing the state of your infrastructure.
Advantages of Testinfra
These are some of the benefits testinfra offers:
Idempotency and Consistency: By writing tests for your infrastructure, you can ensure that your configuration management tools (like Ansible, Salt, Puppet, or Chef) are working correctly and that your servers are in a consistent and predictable state.
Early Detection of Errors: Testinfra helps you catch configuration drifts and errors early in the development cycle, long before they can cause problems in production.
Improved Collaboration: Tests serve as a form of documentation, clearly defining the expected state of your infrastructure and making it easier for teams to collaborate on managing and maintaining servers.
Auditability and compliance: By creating reproducible tests that are created and maintained centrally, testinfra enables infrastructure and security engineers to ensure that the production infrastructure is in the expected states and is not prone to reliability and security incidents that happen due to manual changes.
Modeling infrastructure tests
Fixtures provide a consistent context and reliable environment for tests. In pytest, fixtures are part of the arrange phase. To know more about different phases of pytest, read the “Anatomy of a test”. The host fixture is the central element of testinfra. It represents the system under test and provides access to all the different modules that you can use to inspect the server's state.
Each capability is a module exposed as a method on the host. Here are some of the most commonly used modules available through the host fixture:
host.package(name): To check the status of a package.
host.service(name): To check the status of a service.
host.file(path): To inspect files and directories.
host.socket(uri): To check for listening sockets.
host.user(name): To get information about a user.
host.group(name): To get information about a group.
host.interface(name): To inspect network interfaces.
host.process(name): To find and inspect processes.
host.command(command): To run a command and inspect its output.
You declare the host as a test function argument and then call the module you need. For example, to check if the host is listening on port 22 ( for SSH ), write the test as follows:
def test_ssh_port(host):
assert host.socket("tcp://0.0.0.0:22").is_listening
Getting started
Start by installing the pytest-testinfra package
pip install pytest-testinfra
Let us write a few tests to validate the localhost on which TestInfra is installed
Add the following tests to the file local_infra.py. Description of each test is added as a comment
# Test if the passwd file exists and
# has the right user/group ownership and file permissions
def test_passwd_file(host):
f = host.file("/etc/passwd")
assert f.exists and f.user == "root" and f.group == "root" and f.mode == 0o644
# Check if the package openresty ( an nginx variant with lua ) is installed
# and the package version is 1.2*
def test_openresty_installed(host):
pkg = host.package("openresty")
assert pkg.is_installed
assert pkg.version.startswith("1.2")
# Ensure openresty is running as a service and is enabled
def test_openresty_service(host):
s = host.service("openresty")
assert s.is_running
assert s.is_enabled
Now, let us test these
pytest -v local_infra.py
===================================================== test session starts ========================================
<OUTPUT TRUNCATED>collected 3 items
local_infra.py::test_passwd_file[local] PASSED [ 33%]
local_infra.py::test_openresty_installed[local] PASSED [ 66%]
local_infra.py::test_openresty_service[local] PASSED [100%]
===================================================== 3 passed in 0.13s =========================================
Concept of backends
Testinfra supports a rich set of connection backends. By default, all tests are run locally, but you can target remote hosts or containers with --hosts=<backend specification>.
Supported backends are:
SSH
Paramiko - Python implementation of the SSHv2 protocol
Ansible
Docker
Podman
Kubernetes
Openshift
Salt
WinRM
LXC/LXD
Let's use SSH to connect to a remote host and do some validation
We will log in to a remote host 206.189.137.55 and check if the root login for SSH is disabled or not. Passwordless SSH is already set up for the host. Create a test file test_backend.py and add the following:
def test_ssh_no_root_login(host):
sshd_config = host.file("/etc/ssh/sshd_config")
assert sshd_config.contains("^PermitRootLogin no")
Run the test
pytest -vv --hosts="ssh://206.189.137.55" test_backend.py
========================================== test session starts ===========================
<OUTPUT TRUNCATED>
collected 1 item
test_backend.py::test_ssh_no_root_login[ssh://206.189.137.55] PASSED
============================================ 1 passed in 1.15s ===========================
For most backends except SSH, you will have to install the corresponding testinfra module as follows:
pip install 'pytest-testinfra[ansible,salt]'
You can use pytest parameterization to pass parameters to testinfra tests
import pytest
@pytest.mark.parametrize("name", ["curl", "git"])
def test_utilities_installed(host, name):
assert host.package(name).is_installed
Conclusion
Testinfra belongs in your platform toolbox. It gives you fast, reliable feedback that a box (or image/pod) is configured the way you think it is—before a rollout, after a change, and during drift audits. It scales from “does nginx listen on 80” to “is our org-wide security baseline present, enabled, and locked down,” and because it’s pytest, it integrates well into your workflow and CI with minimal effort.


