Chapitre 1218 minIntermédiaire
Automatiser les équipements réseau : Cisco et pfSense
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer pourquoi un équipement réseau se gère autrement qu'un serveur ;
- écrire l'inventaire d'un groupe de switchs Cisco ;
- créer des VLAN et configurer des ports de façon idempotente ;
- sauvegarder automatiquement les configurations ;
- situer les possibilités et les limites d'Ansible sur pfSense.
3 exercices corrigés
Ansible automatise aussi les switchs, les routeurs et les pare-feu : il se connecte à leur ligne de commande en SSH (ou à leur API) et envoie des commandes depuis le nœud de contrôle, puisque ces équipements n'ont pas de Python. Ce chapitre explique ce qui change avec le réseau, configure les VLAN et les ports des switchs Cisco de Sorbier de façon idempotente, sauvegarde leurs configurations, et fait le point sur pfSense.
Pourquoi le réseau est différent
Sur un serveur, Ansible copie un module Python et l'exécute sur place. Un switch Cisco ne sait pas exécuter de Python : le module tourne donc sur le nœud de contrôle, se connecte à l'équipement, lit sa configuration, calcule les commandes nécessaires et les envoie.
| Serveur Linux | Switch Cisco | |
|---|---|---|
| Où tourne le module | Sur le serveur | Sur le nœud de contrôle |
| Connexion | ssh (un shell) | ansible.netcommon.network_cli (la ligne de commande IOS en SSH), parfois netconf ou httpapi |
| Variable indispensable | — | ansible_network_os: cisco.ios.ios |
| Élévation de privilèges | sudo | enable (ansible_become_method: enable), inutile avec un compte privilege 15 |
| Idempotence | Le module compare l'état du système | Le module compare la configuration courante (running-config) et n'envoie que les lignes qui manquent |
| Facts | setup | cisco.ios.ios_facts |
La collection cisco.ios (et sa dépendance ansible.netcommon) gère IOS et IOS XE, le système des Catalyst 9200 et 9300 de Sorbier. Des collections équivalentes existent pour d'autres constructeurs : arista.eos, junipernetworks.junos, cisco.nxos.
Ses modules sont de deux familles :
- les modules de ressources (
ios_vlans,ios_l2_interfaces,ios_interfaces,ios_static_routes…) décrivent un état structuré, en YAML, comme les modules d'un serveur ; - les modules génériques (
ios_config,ios_command) envoient des lignes de configuration ou des commandesshowtelles quelles.
Préparer les switchs et le nœud de contrôle
Sur chaque switch, un compte local pour Ansible, en plus de la configuration SSH du cours Routage et commutation :
SW-ETAGE1(config)# username ansible privilege 15 algorithm-type scrypt secret Mdp-Switchs-2026!
privilege 15 fait arriver le compte directement en mode privilégié : pas besoin de mot de passe enable. Le mot de passe du compte va dans le coffre d'Ansible.
Sur le nœud de contrôle, la connexion network_cli a besoin d'une bibliothèque SSH en Python, ansible-pylibssh (recommandée) ou, à défaut, paramiko :
pipx inject ansible-core ansible-pylibssh
Sans elle, la première tâche échoue avec « paramiko is not installed ». Connecte-toi ensuite une fois à chaque switch en SSH depuis le bastion pour vérifier et enregistrer son empreinte, comme pour les serveurs.
Les variables du groupe, dans inventaire/group_vars/switchs/vars.yml :
---
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: ansible
ansible_password: "{{ vault_mdp_switchs }}"
et vault_mdp_switchs dans vault.yml, chiffré (chapitre 9).
Recueillir les informations
---
- name: Inventorier les switchs
hosts: switchs
gather_facts: false
tasks:
- name: Lire les facts du switch et ses VLAN
cisco.ios.ios_facts:
gather_subset: min
gather_network_resources: vlans
- name: Afficher le modèle et la version d'IOS
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }} : {{ ansible_facts['net_model'] }},
IOS {{ ansible_facts['net_version'] }}, numéro de série {{ ansible_facts['net_serialnum'] }}
- name: Lancer une commande show
cisco.ios.ios_command:
commands:
- show ip interface brief
register: interfaces
- name: Afficher la sortie
ansible.builtin.debug:
var: interfaces.stdout_lines[0]
gather_facts: false au niveau du play, parce que le module setup est fait pour les serveurs ; on appelle ios_facts à la place. Les facts réseau commencent par net_ : net_hostname, net_model, net_version, net_serialnum, net_interfaces. Avec gather_network_resources, la configuration des VLAN est aussi rendue sous forme structurée, dans ansible_facts['network_resources']['vlans']. Cet inventaire alimente utilement GLPI ou la documentation du réseau.
Créer les VLAN de façon idempotente
Le switch d'étage sw-etage2 doit avoir les VLAN 10, 50, 99, 998 et 999 du plan de Sorbier. Avec le module de ressources ios_vlans :
---
- name: Configurer les VLAN des switchs d'étage
hosts: switchs
gather_facts: false
vars:
vlans_etage:
- vlan_id: 10
name: POSTES
- vlan_id: 50
name: TELEPHONIE
- vlan_id: 99
name: ADMIN
- vlan_id: 998
name: PARKING
- vlan_id: 999
name: NATIF
tasks:
- name: Garantir les VLAN de l'étage
cisco.ios.ios_vlans:
config: "{{ vlans_etage }}"
state: merged
Au premier passage, le module lit les VLAN existants, envoie seulement ce qui manque (des commandes comme vlan 10 puis name POSTES) et renvoie la liste des commandes envoyées dans commands. Au deuxième passage, tout est déjà en place : ok, aucune commande.
Le paramètre state dit comment traiter ce qui existe déjà sur l'équipement :
state | Effet | Usage |
|---|---|---|
merged | Ajoute ou modifie ce qui est décrit, ne touche pas au reste | Le plus prudent : ajouter des VLAN |
replaced | Remplace entièrement la configuration des VLAN décrits | Corriger un VLAN dont des réglages ont dérivé |
overridden | Fait de la liste la seule configuration : les VLAN absents de la liste sont supprimés | Imposer un état complet ; dangereux si la liste est incomplète |
deleted | Supprime les VLAN décrits (ou tous, si config est vide) | Retirer des VLAN |
gathered | Lit la configuration et la renvoie en YAML, sans rien modifier | Documenter, comparer |
rendered | Produit les commandes à partir de config, sans les envoyer | Relire avant d'appliquer |
Configurer les ports
Les ports 1 à 20 de sw-etage2 accueillent un poste et un téléphone IP. Avec ios_config, une boucle sur range génère la configuration de chaque port :
- name: Configurer les ports des postes avec téléphone
cisco.ios.ios_config:
parents: interface GigabitEthernet1/0/{{ item }}
lines:
- description Poste et telephone
- switchport mode access
- switchport access vlan 10
- switchport voice vlan 50
- spanning-tree portfast
loop: "{{ range(1, 21) | list }}"
when: inventory_hostname == 'sw-etage2'
parents désigne le sous-mode (ici l'interface), lines les commandes à y trouver. ios_config compare ces lignes à la configuration courante et n'envoie que celles qui manquent : la tâche est idempotente si les lignes sont écrites exactement comme IOS les affiche dans show running-config. range(1, 21) va de 1 à 20 : la borne de fin est exclue.
Les modifications ne sont faites que dans la running-config. Pour qu'elles survivent à un redémarrage, ajoute à la dernière tâche save_when: modified : le module copie la running-config dans la startup-config si elle a changé.
Sauvegarder les configurations
Sauvegarder chaque soir la configuration de tous les équipements, c'est l'automatisation réseau la plus simple et l'une des plus utiles : après une panne ou une erreur, on sait ce qui a changé et on peut reconstruire.
---
- name: Sauvegarder la configuration des switchs
hosts: switchs
gather_facts: false
tasks:
- name: Enregistrer la running-config sur le bastion
cisco.ios.ios_config:
backup: true
backup_options:
dir_path: "{{ playbook_dir }}/sauvegardes/reseau"
filename: "{{ inventory_hostname }}.cfg"
Chaque exécution réécrit sauvegardes/reseau/sw-etage1.cfg et sw-etage2.cfg sur le bastion. Planifié chaque nuit (une tâche cron sur le bastion, ou un pipeline), et suivi d'un git commit dans un dépôt réservé aux sauvegardes, il donne un historique des configurations : git diff montre ce qui a changé entre deux nuits. Les configurations contiennent des secrets (hachages des mots de passe, clés) : ce dépôt doit être privé et protégé.
Et pfSense ?
pfSense se configure par son interface web, et toute sa configuration est rangée dans un seul fichier XML, /cf/conf/config.xml. Il n'a pas de ligne de commande de configuration comparable à IOS, et Netgate ne publie pas de collection Ansible officielle.
La collection communautaire pfsensible.core comble ce manque : ses modules se connectent en SSH, modifient config.xml et appellent les fonctions PHP de pfSense pour appliquer le changement. Elle gère notamment les alias, les règles de filtrage, la NAT, les VLAN, les interfaces, les utilisateurs et IPsec. Les modules doivent s'exécuter en root : on se connecte avec un compte qui l'est, ou on installe le paquet pfSense-pkg-sudo pour pouvoir utiliser become, que pfSense ne propose pas par défaut. L'inventaire doit aussi indiquer l'interpréteur Python fourni par pfSense (ansible_python_interpreter), dont le chemin dépend de la version. L'interface d'une règle se désigne par son nom affiché dans pfSense (ADMIN) ou son identifiant interne (lan, opt1…).
Un alias et une règle pour autoriser le bastion à joindre les serveurs Windows en WinRM :
- name: Garantir l'alias des serveurs Windows
pfsensible.core.pfsense_alias:
name: SRV_WINDOWS
type: host
address: 192.168.10.177 192.168.10.178 192.168.10.181
descr: Serveurs Windows gérés par Ansible
state: present
- name: Autoriser le bastion vers les serveurs Windows en WinRM HTTPS
pfsensible.core.pfsense_rule:
name: Bastion vers SRV_WINDOWS (WinRM)
action: pass
interface: ADMIN
ipprotocol: inet
protocol: tcp
source: 192.168.10.210
destination: SRV_WINDOWS
destination_port: 5986
state: present
Les règles de filtrage elles-mêmes, la NAT et les VPN de pfSense sont expliqués dans le cours consacré au pare-feu pfSense ; l'architecture des zones, dans Cybersécurité des infrastructures.
Exercices
Exercice 1 sur 3 Les VLAN du cœur Réussi À revoir
Le switch de cœur sw-coeur (adresse de gestion à définir) doit porter les VLAN 10, 20, 30, 40, 50 et 99 du plan de Sorbier, avec leurs noms (POSTES, SERVEURS, ENTREPOT, VISITEURS, TELEPHONIE, ADMIN), plus 998 (PARKING) et 999 (NATIF). Écris la tâche, et dis quel state choisir pour une première mise en place sur un switch déjà en service.
Voir la réponseMasquer la réponse de l'exercice 1
- name: Garantir les VLAN du cœur
cisco.ios.ios_vlans:
config:
- vlan_id: 10
name: POSTES
- vlan_id: 20
name: SERVEURS
- vlan_id: 30
name: ENTREPOT
- vlan_id: 40
name: VISITEURS
- vlan_id: 50
name: TELEPHONIE
- vlan_id: 99
name: ADMIN
- vlan_id: 998
name: PARKING
- vlan_id: 999
name: NATIF
state: merged
merged : il ajoute ce qui manque sans rien supprimer. Sur un switch en service, overridden supprimerait un VLAN oublié dans la liste et couperait les ports qui l'utilisent. Une fois la liste vérifiée (avec state: gathered pour lire l'existant), on peut passer à overridden pour imposer exactement le plan.
Tu avais trouvé ?
Exercice 2 sur 3 Pourquoi toujours changed ? Réussi À revoir
Moussa a écrit la tâche ci-dessous. À chaque exécution, elle est changed sur sw-etage1, alors que la configuration ne change pas. Explique et corrige.
- name: Désactiver les ports inutilisés
cisco.ios.ios_config:
parents: interface range GigabitEthernet1/0/23 - 47
lines:
- sw acc vlan 998
- shut
Voir la réponseMasquer la réponse de l'exercice 2
Deux causes. Les commandes sont abrégées : IOS les accepte mais les enregistre en entier (switchport access vlan 998, shutdown), donc ios_config ne les retrouve jamais dans la configuration et les renvoie. Et interface range n'apparaît pas dans show running-config, qui affiche chaque interface séparément : le parent n'est jamais trouvé. Correction : une boucle sur chaque interface, commandes complètes.
- name: Désactiver les ports inutilisés
cisco.ios.ios_config:
parents: interface GigabitEthernet1/0/{{ item }}
lines:
- switchport access vlan 998
- shutdown
loop: "{{ range(23, 48) | list }}"
range(23, 48) donne les ports 23 à 47.
Tu avais trouvé ?
Exercice 3 sur 3 La sauvegarde nocturne Réussi À revoir
Écris la ligne de crontab du compte de Chloé sur le bastion qui lance le playbook sauvegarde-reseau.yml du projet ~/sorbier-infra chaque nuit à 2 h 30, puis commite les configurations dans le dépôt ~/sauvegardes-reseau. Le playbook écrit dans ce dépôt (dir_path: /home/clambert/sauvegardes-reseau). Que faut-il prévoir pour le mot de passe du coffre et pour la clé SSH, puisque personne n'est là pour les taper ?
Voir la réponseMasquer la réponse de l'exercice 3
30 2 * * * cd ~/sorbier-infra && ansible-playbook sauvegarde-reseau.yml --vault-password-file ~/.config/ansible/coffre-sorbier.txt >> ~/journaux/sauvegarde-reseau.log 2>&1 && cd ~/sauvegardes-reseau && git add -A && git commit -q -m "Sauvegarde du $(date +\%F)"
Dans une crontab, % a un sens spécial (fin de commande) et doit être échappé en \%. Le cd est nécessaire pour que le bon ansible.cfg soit lu. Le PATH de cron est réduit : il peut falloir le chemin complet de la commande (~/.local/bin/ansible-playbook). Sans personne pour taper les secrets : un fichier de mot de passe du coffre protégé (chmod 600), et, pour SSH, le mot de passe du compte des switchs vient déjà du coffre (connexion par mot de passe). Si le commit ne trouve rien à valider (configuration inchangée), git commit renvoie une erreur sans gravité. Une solution plus propre est de lancer la sauvegarde depuis un ordonnanceur (pipeline planifié, AWX, Semaphore), qui garde les secrets et l'historique des exécutions.
Tu avais trouvé ?