Whether in a home lab or a professional infrastructure, network equipment is often overlooked in backup strategies. We think about backing up VMs, data, and servers—but rarely the configurations of switches and Wi-Fi access points. However, in the event of a failure or hardware replacement, not having the configuration on hand can result in hours of work to rebuild the system.

In this article, we’ll set up an Ansible playbook that automatically backs up our network equipment configurations, stores them locally with a 30-day retention period, and sends an HTML report via email after each run.


Target Architecture

For this article, our network infrastructure consists of:

  • 3 HP V1910 switches (Comware firmware) distributed across different zones
  • 2 Cisco Aironet WiFi access points managed in standalone mode
  • 1 Ansible server running Debian/Ubuntu (we’ll call it ANSIBLE-SRV)

The devices are on a VLAN dedicated to management (192.168.X.0/28 for the switches, 192.168.Y.0/28 for the WiFi access points).


Prerequisites

On the Ansible server

# Ansible and the necessary collections
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 for SSH connections with a password
apt install sshpass -y

# expect for complex SSH interactions
apt install expect -y

On network devices

SSH must be enabled on all devices. For HP V1910 (Comware) switches, the legacy SSH connection requires a few additional options in ~/.ssh/config:

Host 192.168.X.*
    KexAlgorithms +diffie-hellman-group1-sha1
    HostKeyAlgorithms +ssh-rsa
    Ciphers +aes128-cbc
    MACs +hmac-sha1

File structure

/etc/ansible/
├── inventory-network.yml       # Device inventory
├── vault/
│   ├── network-secrets.yml     # Encrypted secrets (Ansible Vault)
│   └── .vault_pass             # Vault password file
└── playbooks/
    └── network-backup.yml      # Backup playbook

/opt/network-backups/
├── switches/
│   ├── SWITCH-01/
│   ├── SWITCH-02/
│   └── SWITCH-03/
└── wifi/
    ├── AP-01/
    └── AP-02/

Inventory

# /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

[cisco_devices]
AP-01 ansible_host=192.168.Y.3
AP-02 ansible_host=192.168.Y.4

[cisco_devices:vars]
ansible_user=admin
ansible_password="{{ vault_password_cisco }}"
ansible_port=22
ansible_connection=network_cli
ansible_network_os=ios

Securing Secrets with Ansible Vault

We never store passwords in plain text in the inventory or playbooks. We use Ansible Vault to encrypt secrets:

# Create the secrets file
ansible-vault create /etc/ansible/vault/network-secrets.yml

# File contents
vault_password_switches: MyPassword1
vault_password_cisco: MyPassword2
# Store the vault password in a protected file
echo "MyVaultPassword" > /etc/ansible/vault/.vault_pass
chmod 600 /etc/ansible/vault/.vault_pass

The playbook

---
# ==============================================================================
# Playbook: Backing up network configurations
# Targets: HP V1910 Switches (Comware), Cisco Aironet Access Points
# ==============================================================================

- name: Back up HP V1910 Switch Configurations (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: Create the backup directory
      file:
        path: "{{ backup_dir }}/{{ inventory_hostname }}"
        state: directory
        mode: '0750'
      delegate_to: localhost

    - name: Disable pagination
      ansible.netcommon.cli_command:
        command: screen-length 0 temporary
      register: pagination_off

    - name: Retrieve the complete configuration
      ansible.netcommon.cli_command:
        command: display current-configuration
      register: config_output

    - name: Save the configuration
      copy:
        content: "{{ config_output.stdout }}"
        dest: "{{ backup_dir }}/{{ inventory_hostname }}/{{ inventory_hostname }}_{{ date }}.cfg"
        mode: '0640'
      delegate_to: localhost

    - name: Delete backups older than 30 days
      find:
        paths: "{{ backup_dir }}/{{ inventory_hostname }}"
        age: "30d"
        patterns: "*.cfg"
      register: old_backups
      delegate_to: localhost

    - name: Purge old backups
      file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ old_backups.files }}"
      delegate_to: localhost


- name: Back up Cisco Aironet access point configurations
  hosts: cisco_access_points
  gather_facts: false
  vars:
    backup_dir: "/opt/network-backups/wifi"
    date: "{{ lookup('pipe', 'date +%Y%m%d_%H%M%S') }}"

  tasks:
    - name: Create backup directory
      file:
        path: "{{ backup_dir }}/{{ inventory_hostname }}"
        state: directory
        mode: '0750'
      delegate_to: localhost

    - name: Retrieve full configuration
      cisco.ios.ios_command:
        commands: show running-config
      register: config_output

    - name: Back up the configuration
      copy:
        content: "{{ config_output.stdout[0] }}"
        dest: "{{ backup_dir }}/{{ inventory_hostname }}/{{ inventory_hostname }}_{{ date }}.cfg"
        mode: '0640'
      delegate_to: localhost

    - name: Delete backups older than 30 days
      find:
        paths: "{{ backup_dir }}/{{ inventory_hostname }}"
        age: "30d"
        patterns: "*.cfg"
      register: old_backups
      delegate_to: localhost

    - name: Purge old backups
      file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ old_backups.files }}"
      delegate_to: localhost


- name: Backup report
  hosts: localhost
  gather_facts: true
  vars:
    backup_dir: "/opt/network-backups"

  tasks:
    - name: Count files backed up today
      find:
        paths: "{{ backup_dir }}"
        recurse: true
        age: "-1d"
        patterns: "*.cfg"
      register: todays_backups

    - name: Check switch files
      find:
        paths: "{{ backup_dir }}/switches"
        recurse: true
        age: "-1d"
        patterns: "*.cfg"
      register: switch_backups

    - name: Check Wi-Fi files
      find:
        paths: "{{ backup_dir }}/wifi"
        recurse: true
        age: "-1d"
        patterns: "*.cfg"
      register: wifi_backups

    - name: Build the HTML report
      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;">
              🌐 Network backup report
            </h2>
            <p style="color: #7f8c8d;">Run on {{ ansible_date_time.date }} at {{ 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;">🔀 HP V1910 Switches</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%;">Equipment</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Status</td>
                </tr>
                {% set switch_names = [&#x27;SWITCH-01&#x27;, &#x27;SWITCH-02&#x27;, &#x27;SWITCH-03&#x27;] %}
                {% 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;">🔴 MISSING</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;">📡 Cisco Aironet Access Points</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%;">Equipment</td>
                  <td style="padding: 8px 15px; border-bottom: 1px solid #eee;">Status</td>
                </tr>
                {% set ap_names = [&#x27;AP-01&#x27;, &#x27;AP-02&#x27;] %}
                {% 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;">🔴 MISSING</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 files backed up today: <strong>{{ todays_backups.files | length }}</strong>
              </p>
            </div>

            <p style="color: #95a5a6; font-size: 12px; margin-top: 20px; text-align: center;">
              Automatically generated by Ansible — ANSIBLE-SRV
            </p>
          </div>
          </body>
          </html>- name: Send report via email          
      ansible.builtin.mail:
        host: localhost
        port: 25
        from: ansible@ansible-srv.local
        to: admin@mondomaine.local
        subject: &quot;[Ansible] Network backup - {{ ansible_date_time.date }}&quot;
        secure: never
        body: &quot;{{ recap_message }}&quot;
        subtype: html

Scheduling with cron

crontab -e
# Network backup - Monday and Thursday at 9:15 AM
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 &quot;@/etc/ansible/vault/network-secrets.yml&quot; \
  &gt;&gt; /var/log/ansible-network-backup.log 2&gt;&amp;1

Manual execution

ansible-playbook /etc/ansible/playbooks/network-backup.yml \
  -i /etc/ansible/inventory-network.yml \
  --vault-password-file /etc/ansible/vault/.vault_pass \
  -e &quot;@/etc/ansible/vault/network-secrets.yml&quot;

Result

On each run, the playbook:

  1. Connects to each switch and Wi-Fi access point via SSH
  2. Retrieves the complete configuration (display current-configuration on Comware, show running-config on IOS)
  3. Stores the timestamped file in /opt/network-backups/
  4. Automatically deletes files older than 30 days
  5. Sends an HTML report via email with the status of each device

The HTML report clearly highlights successfully backed-up devices in green and those that failed in red—useful for immediately detecting if a device becomes inaccessible.


Points to Note

Legacy SSH on HP V1910 switches — these older devices only support deprecated SSH algorithms. The options in ~/.ssh/config are essential; otherwise, the connection will fail silently.

Ansible Vault — never commit the .vault_pass file to a Git repository. Add it to your .gitignore.

Permissions — the /opt/network-backups directory must belong to the user running Ansible. Network configuration files may contain sensitive information, hence the chmod 0640.