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 = ['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;">🔴 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 = ['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;">🔴 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: "[Ansible] Network backup - {{ ansible_date_time.date }}"
secure: never
body: "{{ recap_message }}"
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 "@/etc/ansible/vault/network-secrets.yml" \
>> /var/log/ansible-network-backup.log 2>&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 "@/etc/ansible/vault/network-secrets.yml"
Result
On each run, the playbook:
- Connects to each switch and Wi-Fi access point via SSH
- Retrieves the complete configuration (
display current-configurationon Comware,show running-configon IOS) - Stores the timestamped file in
/opt/network-backups/ - Automatically deletes files older than 30 days
- 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.