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 :

  1. Se connecte à chaque switch et borne WiFi via SSH
  2. Récupère la configuration complète (display current-configuration sur Comware, show running-config sur IOS)
  3. Stocke le fichier horodaté dans /opt/network-backups/
  4. Purge automatiquement les fichiers de plus de 30 jours
  5. 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.