Ce guide vous permet de valider automatiquement l’état de vos serveurs en écrivant des tests Python. Vous apprendrez à installer Testinfra, écrire des tests pour les fichiers, services, paquets et configurations réseau, puis exécuter ces tests localement ou sur des serveurs distants. Testinfra résout un problème fondamental du DevOps : comment s’assurer que votre infrastructure correspond réellement à ce que vous avez défini dans vos scripts de configuration.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre le rôle de Testinfra dans l’Infrastructure as Code
- Installer Testinfra et écrire vos premiers tests
- Utiliser les modules File, Service, Package, User, Interface, Socket
- Tester des serveurs distants via SSH, Docker, Ansible ou Kubernetes
- Paramétrer vos tests pour éviter la duplication
- Intégrer Testinfra dans vos pipelines CI/CD
Prérequis : Python 3.9+ et pip installés. Connaissances de base en ligne de commande Linux.
Qu’est-ce que Testinfra ?
Section intitulée « Qu’est-ce que Testinfra ? »Testinfra est une bibliothèque Python qui permet d’écrire des tests unitaires pour valider l’état réel de vos serveurs. Imaginez que vous avez un playbook Ansible qui installe nginx et configure un pare-feu. Comment vérifier que tout s’est bien passé ? Vous pourriez vous connecter en SSH et exécuter quelques commandes manuellement, mais c’est fastidieux et non reproductible. Testinfra automatise cette vérification.
L’outil s’intègre à pytest, le framework de test le plus populaire en Python. Si vous connaissez pytest, vous savez déjà comment structurer vos tests Testinfra. Cette intégration apporte des fonctionnalités puissantes comme la paramétrisation, les fixtures et les rapports détaillés.
| Approche | Sans Testinfra | Avec Testinfra |
|---|---|---|
| Vérification | Connexion manuelle, commandes ad hoc | Tests automatisés, reproductibles |
| Documentation | Implicite, dans la tête de l’admin | Explicite, dans le code des tests |
| Régression | Découverte en production | Détection en CI/CD avant déploiement |
| Conformité | Audit manuel périodique | Validation continue |
Testinfra est l’équivalent Python de Serverspec (Ruby). Il est maintenu par l’équipe pytest-dev et utilisé par de nombreuses entreprises pour valider leurs déploiements automatisés.
Pourquoi tester l’infrastructure ?
Section intitulée « Pourquoi tester l’infrastructure ? »Quand vous déployez un serveur avec Ansible, Puppet ou un script shell, vous faites confiance à vos outils pour appliquer la configuration correctement. Mais plusieurs situations peuvent créer un écart entre l’état désiré et l’état réel.
Les problèmes que Testinfra résout
Section intitulée « Les problèmes que Testinfra résout »La dérive de configuration se produit quand un administrateur modifie manuellement un serveur “juste pour tester”, puis oublie de documenter le changement. Des semaines plus tard, le playbook Ansible est relancé et écrase ces modifications, ou pire, le serveur se comporte différemment des autres. Testinfra détecte ces écarts.
Les erreurs silencieuses surviennent quand un outil de configuration termine sans erreur mais n’applique pas tout correctement. Par exemple, un service peut être installé mais pas démarré, ou un fichier peut exister avec de mauvaises permissions. Testinfra vérifie chaque aspect critique.
La régression de déploiement apparaît quand une mise à jour de dépendance ou un changement d’image de base casse une fonctionnalité. En exécutant les tests Testinfra dans votre pipeline CI/CD, vous détectez ces problèmes avant qu’ils n’atteignent la production.
Installation
Section intitulée « Installation »Testinfra est distribué comme un plugin pytest. L’installation via pip est la méthode recommandée.
Installation de base
Section intitulée « Installation de base »-
Installer Testinfra avec pip
Fenêtre de terminal pip install pytest-testinfraCette commande installe Testinfra et ses dépendances, y compris pytest si vous ne l’avez pas déjà.
-
Vérifier l’installation
Fenêtre de terminal pip show pytest-testinfraRésultat attendu :
Name: pytest-testinfraVersion: 10.2.2 -
Vérifier que pytest reconnaît le plugin
Fenêtre de terminal pytest --versionLa sortie doit mentionner
testinfradans la liste des plugins.
Installation avec des backends spécifiques
Section intitulée « Installation avec des backends spécifiques »Si vous prévoyez de tester des serveurs distants, installez les dépendances correspondantes :
# Pour SSH avec Paramiko (recommandé)pip install pytest-testinfra[paramiko]
# Pour Ansiblepip install pytest-testinfra[ansible]
# Pour Saltpip install pytest-testinfra[salt]
# Tous les backendspip install pytest-testinfra[paramiko,ansible,salt]Écrire votre premier test
Section intitulée « Écrire votre premier test »Les tests Testinfra sont des fonctions Python qui reçoivent un objet host en paramètre. Cet objet représente le serveur à tester et donne accès à tous les modules de Testinfra.
Structure d’un fichier de test
Section intitulée « Structure d’un fichier de test »Créez un fichier test_system.py :
"""Tests de base sur le système."""
def test_passwd_file(host): """Vérifie que le fichier /etc/passwd existe et est bien configuré.""" passwd = host.file("/etc/passwd") assert passwd.exists assert passwd.is_file assert passwd.user == "root" assert passwd.group == "root" assert passwd.mode == 0o644Ce test vérifie cinq propriétés du fichier /etc/passwd : son existence, qu’il s’agit bien d’un fichier (pas un répertoire), son propriétaire, son groupe et ses permissions. Si l’une de ces assertions échoue, pytest affiche un message d’erreur descriptif.
Exécuter les tests
Section intitulée « Exécuter les tests »pytest test_system.py -vRésultat attendu :
======================== test session starts =========================platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0plugins: testinfra-10.2.2collected 1 item
test_system.py::test_passwd_file[local] PASSED [100%]
========================= 1 passed in 0.05s ==========================Le suffixe [local] indique que le test a été exécuté sur la machine locale, sans connexion distante.
Les modules Testinfra
Section intitulée « Les modules Testinfra »Testinfra fournit une vingtaine de modules pour interroger différents aspects d’un système. Chaque module est accessible via l’objet host passé à vos fonctions de test.
Module File
Section intitulée « Module File »Le module File est le plus utilisé. Il permet de vérifier l’existence, le contenu, les permissions et les métadonnées des fichiers.
def test_nginx_config(host): """Vérifie la configuration nginx.""" config = host.file("/etc/nginx/nginx.conf")
# Existence et type assert config.exists assert config.is_file
# Propriétaire et permissions assert config.user == "root" assert config.group == "root" assert config.mode == 0o644
# Contenu assert config.contains("worker_processes")
# Si c'est un lien symbolique link = host.file("/etc/alternatives/editor") if link.is_symlink: print(f"Pointe vers : {link.linked_to}")Propriétés disponibles sur un fichier :
| Propriété | Description | Exemple |
|---|---|---|
exists | Le fichier existe | True |
is_file | C’est un fichier régulier | True |
is_directory | C’est un répertoire | False |
is_symlink | C’est un lien symbolique | False |
is_executable | Permission d’exécution | False |
user | Propriétaire (nom) | "root" |
uid | Propriétaire (ID) | 0 |
group | Groupe (nom) | "root" |
gid | Groupe (ID) | 0 |
mode | Permissions (octal) | 0o644 |
size | Taille en octets | 1024 |
content | Contenu (bytes) | b"..." |
content_string | Contenu (string) | "..." |
md5sum | Hash MD5 | "d41d..." |
sha256sum | Hash SHA256 | "e3b0..." |
mtime | Date de modification | datetime(...) |
linked_to | Cible si symlink | "/usr/bin/vim" |
Module Service
Section intitulée « Module Service »Le module Service vérifie l’état des services système. Il détecte automatiquement le système d’init (systemd, SysV, OpenRC).
def test_nginx_service(host): """Vérifie que nginx est démarré et activé.""" nginx = host.service("nginx")
assert nginx.exists, "Le service nginx n'existe pas" assert nginx.is_running, "nginx n'est pas en cours d'exécution" assert nginx.is_enabled, "nginx ne démarre pas au boot"Sur les systèmes avec systemd, des propriétés supplémentaires sont disponibles :
def test_systemd_service(host): """Vérifie les propriétés systemd d'un service.""" sshd = host.service("sshd")
# Disponible uniquement avec systemd assert sshd.is_valid, "Le fichier unit est invalide" assert not sshd.is_masked, "Le service est masqué"
# Récupérer toutes les propriétés systemd props = sshd.systemd_properties fragment_path = props.get("FragmentPath")Module Package
Section intitulée « Module Package »Le module Package vérifie si un paquet est installé et sa version. Il supporte apt, dnf, yum, apk, pacman, brew et d’autres gestionnaires.
def test_packages_installed(host): """Vérifie que les paquets requis sont installés.""" nginx = host.package("nginx") assert nginx.is_installed
# Vérifier la version python = host.package("python3") assert python.is_installed assert python.version.startswith("3.")Le module détecte automatiquement le gestionnaire de paquets approprié en fonction de la distribution.
Module User et Group
Section intitulée « Module User et Group »Ces modules vérifient l’existence et les propriétés des utilisateurs et groupes système.
def test_application_user(host): """Vérifie que l'utilisateur applicatif existe.""" app_user = host.user("www-data")
assert app_user.exists assert app_user.uid >= 1000 or app_user.uid < 100 # user système ou applicatif assert app_user.group == "www-data" assert app_user.home == "/var/www" assert app_user.shell in ["/usr/sbin/nologin", "/bin/false"]
# Groupes secondaires assert "docker" in app_user.groups or True # optionnel
def test_docker_group(host): """Vérifie le groupe docker.""" docker_group = host.group("docker")
if docker_group.exists: # Liste des membres du groupe members = docker_group.members assert len(members) >= 0Module Interface
Section intitulée « Module Interface »Le module Interface inspecte les interfaces réseau.
def test_network_interfaces(host): """Vérifie la configuration réseau.""" # Interface loopback lo = host.interface("lo") assert lo.exists assert "127.0.0.1" in lo.addresses
# Lister toutes les interfaces interfaces = host.interface.names() assert "lo" in interfaces
# Interface par défaut (celle utilisée pour la route par défaut) default = host.interface.default() if default: print(f"Interface par défaut : {default}") routes = default.routes()Module Socket
Section intitulée « Module Socket »Le module Socket vérifie les sockets en écoute (TCP, UDP, Unix).
def test_listening_ports(host): """Vérifie les ports en écoute.""" # SSH sur le port 22 ssh = host.socket("tcp://22") assert ssh.is_listening
# Nginx sur le port 80 (toutes les IPs) http = host.socket("tcp://0.0.0.0:80") assert http.is_listening
# Socket Unix docker = host.socket("unix:///var/run/docker.sock") if docker.is_listening: clients = docker.clients print(f"Clients connectés : {len(clients)}")
# Lister tous les sockets en écoute all_sockets = host.socket.get_listening_sockets() print(f"Sockets en écoute : {all_sockets}")Module MountPoint
Section intitulée « Module MountPoint »Ce module vérifie les points de montage.
def test_filesystems(host): """Vérifie les systèmes de fichiers montés.""" root = host.mount_point("/")
assert root.exists assert root.filesystem in ["ext4", "xfs", "btrfs"] assert "rw" in root.options # lecture-écriture
# Lister tous les points de montage mounts = host.mount_point.get_mountpoints() for mount in mounts: if mount.filesystem not in ["proc", "sysfs", "tmpfs"]: print(f"{mount.device} -> {mount.path}")Module SystemInfo
Section intitulée « Module SystemInfo »Récupère des informations sur le système.
def test_system_info(host): """Récupère les informations système.""" sysinfo = host.system_info
assert sysinfo.type == "linux" assert sysinfo.distribution in ["ubuntu", "debian", "rocky", "fedora"] assert sysinfo.arch in ["x86_64", "aarch64"]
print(f"Distribution : {sysinfo.distribution} {sysinfo.release}") print(f"Codename : {sysinfo.codename}")Module Sysctl
Section intitulée « Module Sysctl »Vérifie les paramètres kernel.
def test_kernel_params(host): """Vérifie les paramètres de sécurité kernel.""" # Désactiver le forwarding IP (sauf pour les routeurs) assert host.sysctl("net.ipv4.ip_forward") == 0
# Protection contre les attaques SYN flood assert host.sysctl("net.ipv4.tcp_syncookies") == 1Module Process
Section intitulée « Module Process »Inspecte les processus en cours d’exécution.
def test_processes(host): """Vérifie les processus critiques.""" # Trouver le processus nginx master nginx_master = host.process.get(user="root", comm="nginx") print(f"PID nginx master : {nginx_master.pid}")
# Trouver les workers nginx workers = host.process.filter(ppid=nginx_master.pid) assert len(workers) >= 1, "Aucun worker nginx"
# Vérifier la consommation mémoire totale total_mem = sum(w.pmem for w in workers) assert total_mem < 10, "Workers nginx consomment trop de mémoire"Module run (commandes)
Section intitulée « Module run (commandes) »Exécutez n’importe quelle commande et vérifiez le résultat.
def test_command_execution(host): """Exécute des commandes et vérifie les résultats.""" # Commande simple cmd = host.run("uname -s") assert cmd.rc == 0 # code de retour assert cmd.succeeded assert "Linux" in cmd.stdout assert cmd.stderr == ""
# check_output retourne stdout directement kernel = host.check_output("uname -s") assert kernel == "Linux"
# Commande avec arguments (échappement automatique) cmd = host.run("ls -la %s", "/etc/passwd") assert cmd.rc == 0Backends de connexion
Section intitulée « Backends de connexion »Testinfra peut exécuter les tests sur différentes cibles grâce à ses backends de connexion. Par défaut, il utilise le backend local.
Backend local
Section intitulée « Backend local »Les tests s’exécutent sur la machine où pytest tourne.
pytest test_system.py# ou explicitementpytest --hosts=local:// test_system.pyBackend SSH
Section intitulée « Backend SSH »Teste un serveur distant via SSH. Nécessite une authentification par clé.
# Connexion simplepytest --hosts=ssh://192.168.1.100 test_system.py
# Avec un utilisateur spécifiquepytest --hosts=ssh://admin@192.168.1.100 test_system.py
# Avec un fichier de configuration SSHpytest --ssh-config=~/.ssh/config --hosts=ssh://webserver test_system.py
# Avec une clé privée spécifiquepytest --ssh-identity-file=~/.ssh/id_deploy --hosts=ssh://server test_system.py
# Avec timeout personnalisépytest --hosts='ssh://server?timeout=30&controlpersist=120' test_system.pyBackend Docker
Section intitulée « Backend Docker »Teste un conteneur Docker en cours d’exécution.
# Par nom de conteneurpytest --hosts=docker://my-nginx test_system.py
# Par ID de conteneurpytest --hosts=docker://abc123def test_system.py
# En tant qu'utilisateur spécifique dans le conteneurpytest --hosts=docker://root@my-nginx test_system.pyBackend Podman
Section intitulée « Backend Podman »Similaire à Docker, pour les environnements rootless.
pytest --hosts=podman://my-container test_system.pyBackend Ansible
Section intitulée « Backend Ansible »Utilise l’inventaire Ansible pour déterminer les hôtes et les méthodes de connexion.
# Tous les hôtes de l'inventairepytest --hosts=ansible://all test_system.py
# Un groupe spécifiquepytest --hosts=ansible://webservers test_system.py
# Avec un inventaire personnalisépytest --ansible-inventory=/path/to/inventory --hosts=ansible://all test_system.py
# Utiliser Ansible pour l'exécution (plus lent mais plus compatible)pytest --force-ansible --hosts=ansible://all test_system.pyBackend kubectl
Section intitulée « Backend kubectl »Teste des pods Kubernetes.
# Pod avec nom completpytest --hosts='kubectl://mypod-abc123' test_system.py
# Avec namespace et conteneurpytest --hosts='kubectl://mypod?namespace=production&container=app' test_system.py
# Avec un contexte kubeconfig spécifiquepytest --hosts='kubectl://mypod?context=prod-cluster' test_system.pyBackend LXC/LXD
Section intitulée « Backend LXC/LXD »Teste des conteneurs LXC ou LXD.
pytest --hosts=lxc://my-container test_system.pyBackend WinRM
Section intitulée « Backend WinRM »Teste des serveurs Windows (nécessite pywinrm).
pytest --hosts='winrm://Administrator:Password@192.168.1.100' test_system.pyParamétrer vos tests
Section intitulée « Paramétrer vos tests »Pytest offre une fonctionnalité puissante : la paramétrisation. Elle permet d’exécuter le même test avec différentes données d’entrée, évitant la duplication de code.
Paramétrisation basique
Section intitulée « Paramétrisation basique »import pytest
@pytest.mark.parametrize("filepath,expected_user", [ ("/etc/passwd", "root"), ("/etc/shadow", "root"), ("/etc/group", "root"), ("/etc/hostname", "root"),])def test_system_files_ownership(host, filepath, expected_user): """Vérifie que les fichiers système appartiennent à root.""" f = host.file(filepath) assert f.exists, f"Le fichier {filepath} n'existe pas" assert f.user == expected_user, f"{filepath} appartient à {f.user}, attendu {expected_user}"Ce test s’exécute quatre fois, une pour chaque fichier listé.
Paramétrisation pour plusieurs hôtes
Section intitulée « Paramétrisation pour plusieurs hôtes »Pour tester plusieurs serveurs avec les mêmes tests :
pytest --hosts=ssh://server1,ssh://server2,ssh://server3 test_system.pyChaque test s’exécute sur chaque serveur.
Configuration avancée avec conftest.py
Section intitulée « Configuration avancée avec conftest.py »Créez un fichier conftest.py pour personnaliser le comportement :
"""Configuration pytest pour Testinfra."""import pytestimport testinfra
def pytest_addoption(parser): """Ajoute des options personnalisées.""" parser.addoption( "--environment", action="store", default="dev", help="Environnement cible : dev, staging, prod" )
@pytest.fixture(scope="module")def environment(request): """Retourne l'environnement cible.""" return request.config.getoption("--environment")
@pytest.fixturedef expected_packages(environment): """Retourne les paquets attendus selon l'environnement.""" base_packages = ["nginx", "curl", "vim"] if environment == "prod": return base_packages + ["fail2ban", "auditd"] return base_packagesUtilisez ensuite ces fixtures dans vos tests :
def test_required_packages(host, expected_packages): """Vérifie que les paquets requis sont installés.""" for pkg_name in expected_packages: pkg = host.package(pkg_name) assert pkg.is_installed, f"Paquet manquant : {pkg_name}"API Python
Section intitulée « API Python »En plus de l’utilisation via pytest, Testinfra expose une API Python pour créer des hosts programmatiquement.
import testinfra
# Créer un host localhost = testinfra.get_host("local://")
# Vérifier un fichierif host.file("/etc/nginx/nginx.conf").exists: print("nginx est configuré")
# Exécuter une commanderesult = host.run("systemctl is-active nginx")print(f"nginx est : {result.stdout.strip()}")
# Créer un host SSHremote = testinfra.get_host("ssh://admin@192.168.1.100")print(f"Système distant : {remote.system_info.distribution}")Cette API est utile pour écrire des scripts de validation ou intégrer Testinfra dans d’autres outils.
Intégration CI/CD
Section intitulée « Intégration CI/CD »Testinfra s’intègre naturellement dans les pipelines CI/CD. Voici quelques exemples de configuration.
GitLab CI
Section intitulée « GitLab CI »test-infrastructure: stage: test image: python:3.12 before_script: - pip install pytest-testinfra[paramiko] script: - pytest tests/ --hosts=ssh://$DEPLOY_USER@$DEPLOY_HOST -v --junit-xml=report.xml artifacts: reports: junit: report.xml only: - mainGitHub Actions
Section intitulée « GitHub Actions »name: Infrastructure Testson: [push, pull_request]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12'
- name: Install Testinfra run: pip install pytest-testinfra
- name: Run tests run: pytest tests/ -vRapport JUnit
Section intitulée « Rapport JUnit »Pour intégrer les résultats dans votre système CI :
pytest tests/ --junit-xml=results.xml -vDépannage
Section intitulée « Dépannage »Problèmes courants et solutions
Section intitulée « Problèmes courants et solutions »| Problème | Cause probable | Solution |
|---|---|---|
ModuleNotFoundError: testinfra | Testinfra pas installé dans l’environnement actif | pip install pytest-testinfra dans le bon venv |
Connection refused (SSH) | Serveur SSH non accessible | Vérifier la connectivité avec ssh user@host |
Permission denied | Clé SSH non autorisée | Ajouter la clé publique à authorized_keys |
No such container (Docker) | Conteneur arrêté ou mal nommé | Vérifier avec docker ps |
Service not found | Nom de service incorrect ou système d’init différent | Vérifier le nom exact avec systemctl list-units |
| Tests très lents via SSH | Pas de ControlPersist | Utiliser --hosts='ssh://host?controlpersist=120' |
Activer le mode verbose
Section intitulée « Activer le mode verbose »pytest tests/ -v --tb=longL’option --tb=long affiche la traceback complète en cas d’erreur.
Déboguer un test spécifique
Section intitulée « Déboguer un test spécifique »# Exécuter un seul testpytest tests/test_system.py::test_passwd_file -v
# Avec sortie des printspytest tests/ -v -sBonnes pratiques
Section intitulée « Bonnes pratiques »Organisation des fichiers de test
Section intitulée « Organisation des fichiers de test »tests/├── conftest.py # Configuration et fixtures├── test_base.py # Tests communs à tous les serveurs├── test_webserver.py # Tests spécifiques aux serveurs web├── test_database.py # Tests spécifiques aux bases de données└── test_security.py # Tests de durcissementNommage des tests
Section intitulée « Nommage des tests »Utilisez des noms descriptifs qui expliquent ce qui est vérifié :
# ❌ Mauvaisdef test_1(host): ...
# ✅ Bondef test_ssh_service_is_running_and_enabled(host): ...Messages d’assertion clairs
Section intitulée « Messages d’assertion clairs »# ❌ Message par défaut peu informatifassert nginx.is_running
# ✅ Message expliciteassert nginx.is_running, "Le service nginx devrait être en cours d'exécution"Exécuter avec sudo
Section intitulée « Exécuter avec sudo »Certains tests nécessitent les privilèges root :
pytest --sudo tests/Ou dans le code :
def test_shadow_file(host): """Vérifie /etc/shadow (nécessite sudo).""" with host.sudo(): shadow = host.file("/etc/shadow") assert shadow.exists assert shadow.mode == 0o640À retenir
Section intitulée « À retenir »- Testinfra v10.2.2 permet de valider l’état réel de vos serveurs avec des tests Python
- Basé sur pytest, il bénéficie de la paramétrisation, des fixtures et des rapports
- Installation :
pip install pytest-testinfra - Modules principaux : File, Service, Package, User, Group, Interface, Socket, MountPoint
- Backends : local, SSH, Docker, Podman, Ansible, kubectl, LXC, WinRM
- Intégration naturelle dans les pipelines CI/CD pour valider chaque déploiement
- Utilisez
--sudopour les tests nécessitant les privilèges root - La paramétrisation pytest évite la duplication de code pour tester plusieurs éléments
Prochaines étapes
Section intitulée « Prochaines étapes »Ressources
Section intitulée « Ressources »- Documentation officielle : testinfra.readthedocs.io
- Repository GitHub : github.com/pytest-dev/pytest-testinfra
- Changelog : testinfra.readthedocs.io/en/latest/changelog.html
- Documentation pytest : docs.pytest.org