Dans un homelab ou une infrastructure pro, les équipements réseau sont souvent les grands oubliés des stratégies de sauvegarde. On pense à sauvegarder les VMs, les données, les serveurs — mais rarement les configurations des switches et des bornes WiFi. Pourtant, en cas de panne ou de remplacement matériel, ne pas avoir la configuration sous la main peut coûter des heures de reconstruction.
Dans cet article, on va mettre en place un playbook Ansible qui sauvegarde automatiquement les configurations de nos équipements réseau, les stocke localement avec une rétention de 30 jours, et envoie un rapport HTML par mail à chaque exécution.
Architecture cible
Pour cet article, notre infrastructure réseau se compose de :
- 3 switches HP V1910 (firmware Comware) répartis sur différentes zones
- 2 bornes WiFi Cisco Aironet gérées en mode autonome
- 1 serveur Ansible sous Debian/Ubuntu (on l’appellera
ANSIBLE-SRV)
Les équipements sont sur un VLAN dédié à la gestion (192.168.X.0/28 pour les switches, 192.168.Y.0/28 pour les bornes WiFi).
Prérequis
Sur le serveur Ansible
# Ansible et les collections nécessaires
pip install ansible --break-system-packages
ansible-galaxy collection install ansible.netcommon
ansible-galaxy collection install community.network
ansible-galaxy collection install cisco.ios
# sshpass pour les connexions SSH avec mot de passe
apt install sshpass -y
# expect pour les interactions SSH complexes
apt install expect -y
Sur les équipements réseau
SSH doit être activé sur tous les équipements. Pour les switches HP V1910 (Comware), la connexion SSH legacy nécessite quelques options supplémentaires dans ~/.ssh/config :
Host 192.168.X.*
KexAlgorithms +diffie-hellman-group1-sha1
HostKeyAlgorithms +ssh-rsa
Ciphers +aes128-cbc
MACs +hmac-sha1
Structure des fichiers
/etc/ansible/
├── inventory-network.yml # Inventaire des équipements
├── vault/
│ ├── network-secrets.yml # Secrets chiffrés (Ansible Vault)
│ └── .vault_pass # Fichier mot de passe vault
└── playbooks/
└── network-backup.yml # Playbook de sauvegarde
/opt/network-backups/
├── switches/
│ ├── SWITCH-01/
│ ├── SWITCH-02/
│ └── SWITCH-03/
└── wifi/
├── AP-01/
└── AP-02/
Inventaire
# /etc/ansible/inventory-network.yml
[switches_v1910]
SWITCH-01 ansible_host=192.168.X.3
SWITCH-02 ansible_host=192.168.X.5
SWITCH-03 ansible_host=192.168.X.6
[switches_v1910:vars]
ansible_user=admin
ansible_password="{{ vault_password_switches }}"
ansible_port=22
ansible_connection=network_cli
ansible_network_os=community.network.ce
[bornes_cisco]
AP-01 ansible_host=192.168.Y.3
AP-02 ansible_host=192.168.Y.4
[bornes_cisco:vars]
ansible_user=admin
ansible_password="{{ vault_password_cisco }}"
ansible_port=22
ansible_connection=network_cli
ansible_network_os=ios
Sécurisation des secrets avec Ansible Vault
On ne stocke jamais les mots de passe en clair dans l’inventaire ou les playbooks. On utilise Ansible Vault pour chiffrer les secrets :
# Créer le fichier de secrets
ansible-vault create /etc/ansible/vault/network-secrets.yml
# Contenu du fichier
vault_password_switches: MonMotDePasse1
vault_password_cisco: MonMotDePasse2
# Stocker le mot de passe vault dans un fichier protégé
echo "MonMotDePasseVault" > /etc/ansible/vault/.vault_pass
chmod 600 /etc/ansible/vault/.vault_pass
Le playbook
---
# ==============================================================================
# Playbook : Sauvegarde des configurations réseau
# Cibles : Switches HP V1910 (Comware), Bornes Cisco Aironet
# ==============================================================================
- name: Sauvegarde configurations switches HP V1910 (Comware)
hosts: switches_v1910
gather_facts: false
vars:
backup_dir: "/opt/network-backups/switches"
date: "{{ lookup('pipe', 'date +%Y%m%d_%H%M%S') }}"
tasks:
- name: Créer le répertoire de sauvegarde
file:
path: "{{ backup_dir }}/{{ inventory_hostname }}"
state: directory
mode: '0750'
delegate_to: localhost
- name: Désactiver la pagination
ansible.netcommon.cli_command:
command: screen-length 0 temporary
register: pagination_off
- name: Récupérer la configuration complète
ansible.netcommon.cli_command:
command: display current-configuration
register: config_output
- name: Sauvegarder la configuration
copy:
content: "{{ config_output.stdout }}"
dest: "{{ backup_dir }}/{{ inventory_hostname }}/{{ inventory_hostname }}_{{ date }}.cfg"
mode: '0640'
delegate_to: localhost
- name: Supprimer les sauvegardes de plus de 30 jours
find:
paths: "{{ backup_dir }}/{{ inventory_hostname }}"
age: "30d"
patterns: "*.cfg"
register: old_backups
delegate_to: localhost
- name: Purger les anciennes sauvegardes
file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
delegate_to: localhost
- name: Sauvegarde configurations bornes Cisco Aironet
hosts: bornes_cisco
gather_facts: false
vars:
backup_dir: "/opt/network-backups/wifi"
date: "{{ lookup('pipe', 'date +%Y%m%d_%H%M%S') }}"
tasks:
- name: Créer le répertoire de sauvegarde
file:
path: "{{ backup_dir }}/{{ inventory_hostname }}"
state: directory
mode: '0750'
delegate_to: localhost
- name: Récupérer la configuration complète
cisco.ios.ios_command:
commands: show running-config
register: config_output
- name: Sauvegarder la configuration
copy:
content: "{{ config_output.stdout[0] }}"
dest: "{{ backup_dir }}/{{ inventory_hostname }}/{{ inventory_hostname }}_{{ date }}.cfg"
mode: '0640'
delegate_to: localhost
- name: Supprimer les sauvegardes de plus de 30 jours
find:
paths: "{{ backup_dir }}/{{ inventory_hostname }}"
age: "30d"
patterns: "*.cfg"
register: old_backups
delegate_to: localhost
- name: Purger les anciennes sauvegardes
file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
delegate_to: localhost
- name: Rapport de sauvegarde
hosts: localhost
gather_facts: true
vars:
backup_dir: "/opt/network-backups"
tasks:
- name: Compter les fichiers sauvegardés aujourd'hui
find:
paths: "{{ backup_dir }}"
recurse: true
age: "-1d"
patterns: "*.cfg"
register: todays_backups
- name: Vérifier fichiers switches
find:
paths: "{{ backup_dir }}/switches"
recurse: true
age: "-1d"
patterns: "*.cfg"
register: switch_backups
- name: Vérifier fichiers wifi
find:
paths: "{{ backup_dir }}/wifi"
recurse: true
age: "-1d"
patterns: "*.cfg"
register: wifi_backups
- name: Construire le rapport HTML
ansible.builtin.set_fact:
recap_message: |
<html>
<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; padding: 20px;">
<div style="max-width: 800px; margin: auto; background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h2 style="color: #2c3e50; border-bottom: 2px solid #e67e22; padding-bottom: 10px;">
🌐 Rapport de sauvegarde réseau
</h2>
<p style="color: #7f8c8d;">Exécuté le {{ ansible_date_time.date }} à {{ ansible_date_time.time }}</p>
<div style="margin-top: 20px; border: 1px solid #ddd; border-radius: 6px; overflow: hidden;">
<div style="background-color: #2980b9; color: white; padding: 10px 15px;">
<h3 style="margin: 0;">🔀 Switches HP V1910</h3>
</div>
<table style="width: 100%; border-collapse: collapse;">
<tr style="background-color: #f0f0f0; font-weight: bold;">
<td style="padding: 8px 15px; border-bottom: 1px solid #eee; width: 60%;">Équipement</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Statut</td>
</tr>
{% set switch_names = ['SWITCH-01', 'SWITCH-02', 'SWITCH-03'] %}
{% for sw in switch_names %}
{% set ns = namespace(found=false) %}
{% for f in switch_backups.files %}
{% if sw in f.path %}{% set ns.found = true %}{% endif %}
{% endfor %}
<tr style="background-color: {% if loop.index is odd %}#f9f9f9{% else %}white{% endif %};">
<td style="padding: 8px 15px; border-bottom: 1px solid #eee; font-weight: bold;">{{ sw }}</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
{% if ns.found %}
<span style="color: #27ae60;">✅ OK</span>
{% else %}
<span style="color: #e74c3c;">🔴 MANQUANT</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
<div style="margin-top: 20px; border: 1px solid #ddd; border-radius: 6px; overflow: hidden;">
<div style="background-color: #8e44ad; color: white; padding: 10px 15px;">
<h3 style="margin: 0;">📡 Bornes Cisco Aironet</h3>
</div>
<table style="width: 100%; border-collapse: collapse;">
<tr style="background-color: #f0f0f0; font-weight: bold;">
<td style="padding: 8px 15px; border-bottom: 1px solid #eee; width: 60%;">Équipement</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Statut</td>
</tr>
{% set ap_names = ['AP-01', 'AP-02'] %}
{% for ap in ap_names %}
{% set ns = namespace(found=false) %}
{% for f in wifi_backups.files %}
{% if ap in f.path %}{% set ns.found = true %}{% endif %}
{% endfor %}
<tr style="background-color: {% if loop.index is odd %}#f9f9f9{% else %}white{% endif %};">
<td style="padding: 8px 15px; border-bottom: 1px solid #eee; font-weight: bold;">{{ ap }}</td>
<td style="padding: 8px 15px; border-bottom: 1px solid #eee;">
{% if ns.found %}
<span style="color: #27ae60;">✅ OK</span>
{% else %}
<span style="color: #e74c3c;">🔴 MANQUANT</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
<div style="margin-top: 20px; background-color: #ecf0f1; border-radius: 6px; padding: 15px;">
<p style="margin: 0; font-size: 14px; color: #2c3e50;">
📊 Total fichiers sauvegardés aujourd'hui : <strong>{{ todays_backups.files | length }}</strong>
</p>
</div>
<p style="color: #95a5a6; font-size: 12px; margin-top: 20px; text-align: center;">
Généré automatiquement par Ansible — ANSIBLE-SRV
</p>
</div>
</body>
</html>
- name: Envoyer le rapport par mail
ansible.builtin.mail:
host: localhost
port: 25
from: ansible@ansible-srv.local
to: admin@mondomaine.local
subject: "[Ansible] Sauvegarde réseau - {{ ansible_date_time.date }}"
secure: never
body: "{{ recap_message }}"
subtype: html
Planification avec cron
crontab -e
# Backup réseau - lundi et jeudi à 9h15
15 9 * * 1,4 ansible-playbook /etc/ansible/playbooks/network-backup.yml \
-i /etc/ansible/inventory-network.yml \
--vault-password-file /etc/ansible/vault/.vault_pass \
-e "@/etc/ansible/vault/network-secrets.yml" \
>> /var/log/ansible-network-backup.log 2>&1
Lancement manuel
ansible-playbook /etc/ansible/playbooks/network-backup.yml \
-i /etc/ansible/inventory-network.yml \
--vault-password-file /etc/ansible/vault/.vault_pass \
-e "@/etc/ansible/vault/network-secrets.yml"
Résultat
À chaque exécution, le playbook :
- Se connecte à chaque switch et borne WiFi via SSH
- Récupère la configuration complète (
display current-configurationsur Comware,show running-configsur IOS) - Stocke le fichier horodaté dans
/opt/network-backups/ - Purge automatiquement les fichiers de plus de 30 jours
- Envoie un rapport HTML par mail avec le statut de chaque équipement
Le rapport HTML présente clairement en vert les équipements sauvegardés avec succès et en rouge ceux qui auraient échoué — pratique pour détecter immédiatement si un équipement devient inaccessible.
Points d’attention
SSH legacy sur les switches HP V1910 — ces équipements anciens ne supportent que des algorithmes SSH dépréciés. Les options dans ~/.ssh/config sont indispensables sinon la connexion échoue silencieusement.
Ansible Vault — ne jamais committer le fichier .vault_pass dans un dépôt Git. Ajoutez-le à votre .gitignore.
Permissions — le répertoire /opt/network-backups doit appartenir à l’utilisateur qui exécute Ansible. Les fichiers de config réseau peuvent contenir des informations sensibles, d’où le chmod 0640.