From 59c0fda15bebc85218a72e5581aedeae6a021e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20L=C3=A9vy?= Date: Thu, 12 Jun 2025 21:43:10 +0200 Subject: [PATCH 01/10] Ajout de ZFS-backup --- playbooks/zfsbackup.yml | 5 + roles/zfs-backup/defaults/main.yml | 4 + roles/zfs-backup/handlers/main.yml | 5 + roles/zfs-backup/tasks/main.yml | 32 +++++ .../templates/zfs-backup.service.j2 | 18 +++ .../zfs-backup/templates/zfs-backup.timer.j2 | 8 ++ roles/zfs-backup/templates/zfs-snapshot-nas | 123 ++++++++++++++++++ 7 files changed, 195 insertions(+) create mode 100755 playbooks/zfsbackup.yml create mode 100644 roles/zfs-backup/defaults/main.yml create mode 100644 roles/zfs-backup/handlers/main.yml create mode 100644 roles/zfs-backup/tasks/main.yml create mode 100644 roles/zfs-backup/templates/zfs-backup.service.j2 create mode 100644 roles/zfs-backup/templates/zfs-backup.timer.j2 create mode 100755 roles/zfs-backup/templates/zfs-snapshot-nas diff --git a/playbooks/zfsbackup.yml b/playbooks/zfsbackup.yml new file mode 100755 index 0000000..e501c2a --- /dev/null +++ b/playbooks/zfsbackup.yml @@ -0,0 +1,5 @@ +#!/usr/bin/env ansible-playbook +--- +- hosts: caradoc.adm.auro.re + roles: + - zfs-backup diff --git a/roles/zfs-backup/defaults/main.yml b/roles/zfs-backup/defaults/main.yml new file mode 100644 index 0000000..56b0299 --- /dev/null +++ b/roles/zfs-backup/defaults/main.yml @@ -0,0 +1,4 @@ +--- +zfs_backup: + - scriptpath: /var/zfs-backup-nas +... diff --git a/roles/zfs-backup/handlers/main.yml b/roles/zfs-backup/handlers/main.yml new file mode 100644 index 0000000..60f493a --- /dev/null +++ b/roles/zfs-backup/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Run systemd daemon-reload + systemd: + daemon_reload: true +... diff --git a/roles/zfs-backup/tasks/main.yml b/roles/zfs-backup/tasks/main.yml new file mode 100644 index 0000000..0d27be0 --- /dev/null +++ b/roles/zfs-backup/tasks/main.yml @@ -0,0 +1,32 @@ +--- +- name: Copy files for zfs-backup + template: + src: "{{ item }}.j2" + dest: /etc/systemd/system/{{ item }} + owner: root + group: root + mode: u=rw,g=r,o= + loop: + - zfs-backup.service + - zfs-backup.timer + notify: + - Run systemd daemon-reload + +- name: Copie du script + template: + src: zfs-snapshot-nas + dest: "{{ zfs_backup.scriptpath }}" + owner: root + group: root + mode: u=rx,g=r,o= + +- name: Run systemd deamon-reload + systemd: + daemon_reload: true + +- name: Start and enable ZFS-backup timer + systemd: + name: zfs-backup.timer + state: started + enabled: true +... diff --git a/roles/zfs-backup/templates/zfs-backup.service.j2 b/roles/zfs-backup/templates/zfs-backup.service.j2 new file mode 100644 index 0000000..5bfacdf --- /dev/null +++ b/roles/zfs-backup/templates/zfs-backup.service.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=Service pour ZFS-backup +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +User=root + +Nice=19 +CPUSchedulingPolicy=batch + +Restart=no + +LogRateLimitIntervalSec=0 + +ExecStartPre=/usr/bin/bash {{ zfs_backup.scriptpath }} nextcloud +ExecStart=/usr/bin/bash {{ zfs_backup.scriptpath }} gitea diff --git a/roles/zfs-backup/templates/zfs-backup.timer.j2 b/roles/zfs-backup/templates/zfs-backup.timer.j2 new file mode 100644 index 0000000..abb0a1a --- /dev/null +++ b/roles/zfs-backup/templates/zfs-backup.timer.j2 @@ -0,0 +1,8 @@ +[Unit] +Description=Timer for ZFS-backup + +[Timer] +OnCalendar=daily + +[Install] +WantedBy=timers.target diff --git a/roles/zfs-backup/templates/zfs-snapshot-nas b/roles/zfs-backup/templates/zfs-snapshot-nas new file mode 100755 index 0000000..e3eeffd --- /dev/null +++ b/roles/zfs-backup/templates/zfs-snapshot-nas @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +if [ $# -ne 1 ] +then + echo "USAGE: $0 " + exit 1 +fi + +TANK="$1" + +IS_TANK_EXIST=$(zfs list | grep -c "tank/${TANK} ") + +if [ "${IS_TANK_EXIST}" -ne 1 ] +then + echo "${TANK} n'existe pas. Arrêt." + exit 1 +fi + + +TODAY=$(date "+%Y-%m-%d") +TODAY_EPOCH=$(date -d "${TODAY}" +%s) + +# # 1. On fait les snapshots sur le ZFS du NAS. +# /sbin/zfs snapshot "tank/${TANK}@${TODAY}" + + +# 2. On envoie les snapshots sur le ZFS de backup. +while true +do + + # Au préalable, on regarde si un envoi a été interrompu. + TOKEN=$(ssh perceval.adm.auro.re zfs list -o receive_resume_token "tank/${TANK}_backup" | tail -n 1) + + if [ "${#TOKEN}" -gt 15 ] + then + echo "Un envoi a été interrompu. Reprise." + + zfs send -t "${TOKEN}" | pv -trb | ssh perceval.adm.auro.re zfs recv -s -u "tank/${TANK}_backup" + + if [ $? -ne 0 ] + then + echo "La reprise s'est mal déroulée. Arrêt." + exit 1 + fi + fi + + # On récupère les dernières snapshots envoyées sur backup. + LAST_SNAPSHOT=$(ssh perceval.adm.auro.re zfs list -t snapshot \ + | grep "tank/${TANK}_backup" \ + | cut -d' ' -f1 | cut -d'@' -f2 \ + | sort | tail -n1) + + LAST_SNAPSHOT_EPOCH=$(date -d "${LAST_SNAPSHOT}" "+%s") + + # Si la dernière backup envoyée est celle d'aujourd'hui: On sort. + if [ "${LAST_SNAPSHOT_EPOCH}" -ge "${TODAY_EPOCH}" ] + then + echo "La backup distance ${TANK} ${LAST_SNAPSHOT} est suffisament récente. Fin." + break + fi + + # Sinon, on envoie une backup supplémentaire à partir de la dernière snapshot. + NEW_SNAPSHOT=$(date -d "${LAST_SNAPSHOT} +1day" "+%Y-%m-%d") + + echo "Envoi de la backup ${TANK} ${NEW_SNAPSHOT}." + + zfs send -i "tank/${TANK}@${LAST_SNAPSHOT}" "tank/${TANK}@${NEW_SNAPSHOT}" \ + | pv -trb | ssh perceval.adm.auro.re zfs recv -s -u "tank/${TANK}_backup" + + if [ $? -ne 0 ] + then + echo "L'envoi s'est mal déroulé. Arrêt." + exit 1 + fi + +done + + +# # 3. On ne garde que les 15 dernières snapshots ZFS +# LIMIT=$(date -d "${TODAY} -15days" "+%Y-%m-%d") +# LIMIT_EPOCH=$(date -d "${LIMIT}" "+%s") +# +# while true +# do +# +# # On vérifie qu'il existe au moins 15 backups +# COUNT=$(zfs list -t snapshot \ +# | grep -c "tank/${TANK}") +# +# if [ "${COUNT}" -le 15 ] +# then +# echo "Il y a moins de 16 backups. Fin." +# break +# fi +# +# +# # On récupère la plus vieille snapshot +# OLDEST_SNAPSHOT=$(zfs list -t snapshot \ +# | grep "tank/${TANK}" \ +# | cut -d' ' -f1 | cut -d'@' -f2 \ +# | sort | head -n1) +# +# OLDEST_SNAPSHOT_EPOCH=$(date -d "${OLDEST_SNAPSHOT}" "+%s") +# +# # Sanity-check: Si la plus vieille backup est celle d'il y a moins de 15 jours: On sort. +# if [ "${OLDEST_SNAPSHOT_EPOCH}" -ge "${LIMIT_EPOCH}" ] +# then +# echo "La backup locale ${TANK} ${OLDEST_SNAPSHOT} est suffisament récente. Fin." +# break +# fi +# +# # Sinon, on supprime la plus vieille snapshot. +# echo "Suppression de la backup ${TANK} ${OLDEST_SNAPSHOT}." +# +# zfs destroy "tank/${TANK}@${OLDEST_SNAPSHOT}" +# +# if [ $? -ne 0 ] +# then +# echo "La suppression s'est mal déroulée. Arrêt." +# exit 1 +# fi +# +# done From 953b29c51d25844cecf9551a6a8bb8ebd1974f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20L=C3=A9vy?= Date: Thu, 10 Jul 2025 22:12:18 +0200 Subject: [PATCH 02/10] Retrait de chapalux des hosts --- hosts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts b/hosts index c4ba277..3c257c4 100644 --- a/hosts +++ b/hosts @@ -355,7 +355,7 @@ ps-4-3.borne.auro.re caradoc.adm.auro.re [edc_pve] -chapalux.adm.auro.re +#chapalux.adm.auro.re [edc_vm] routeur-edc.adm.auro.re From d93dee83f0e1028a08a066727b35791efd97093b Mon Sep 17 00:00:00 2001 From: korenstin Date: Thu, 7 Aug 2025 23:39:04 +0200 Subject: [PATCH 03/10] Config backup/prune zfs --- playbooks/zfsprune.yml | 5 + roles/zfs-backup/defaults/main.yml | 2 +- .../{templates => files}/zfs-snapshot-nas | 92 +++++++++---------- roles/zfs-backup/handlers/main.yml | 6 ++ roles/zfs-backup/tasks/main.yml | 15 +-- .../templates/zfs-backup.service.j2 | 4 + .../zfs-backup/templates/zfs-backup.timer.j2 | 2 + roles/zfs-prune/defaults/main.yml | 4 + roles/zfs-prune/files/zfs-remove-old-snapshot | 66 +++++++++++++ roles/zfs-prune/handlers/main.yml | 11 +++ roles/zfs-prune/tasks/main.yml | 23 +++++ .../zfs-prune/templates/zfs-prune.service.j2 | 23 +++++ roles/zfs-prune/templates/zfs-prune.timer.j2 | 11 +++ shell.nix | 10 ++ 14 files changed, 215 insertions(+), 59 deletions(-) create mode 100755 playbooks/zfsprune.yml rename roles/zfs-backup/{templates => files}/zfs-snapshot-nas (59%) create mode 100644 roles/zfs-prune/defaults/main.yml create mode 100755 roles/zfs-prune/files/zfs-remove-old-snapshot create mode 100644 roles/zfs-prune/handlers/main.yml create mode 100644 roles/zfs-prune/tasks/main.yml create mode 100644 roles/zfs-prune/templates/zfs-prune.service.j2 create mode 100644 roles/zfs-prune/templates/zfs-prune.timer.j2 create mode 100644 shell.nix diff --git a/playbooks/zfsprune.yml b/playbooks/zfsprune.yml new file mode 100755 index 0000000..1e3519f --- /dev/null +++ b/playbooks/zfsprune.yml @@ -0,0 +1,5 @@ +#!/usr/bin/env ansible-playbook +--- +- hosts: perceval.adm.auro.re + roles: + - zfs-prune diff --git a/roles/zfs-backup/defaults/main.yml b/roles/zfs-backup/defaults/main.yml index 56b0299..0c3fcba 100644 --- a/roles/zfs-backup/defaults/main.yml +++ b/roles/zfs-backup/defaults/main.yml @@ -1,4 +1,4 @@ --- zfs_backup: - - scriptpath: /var/zfs-backup-nas + scriptpath: /var/zfs-backup-nas ... diff --git a/roles/zfs-backup/templates/zfs-snapshot-nas b/roles/zfs-backup/files/zfs-snapshot-nas similarity index 59% rename from roles/zfs-backup/templates/zfs-snapshot-nas rename to roles/zfs-backup/files/zfs-snapshot-nas index e3eeffd..15386f1 100755 --- a/roles/zfs-backup/templates/zfs-snapshot-nas +++ b/roles/zfs-backup/files/zfs-snapshot-nas @@ -21,7 +21,7 @@ TODAY=$(date "+%Y-%m-%d") TODAY_EPOCH=$(date -d "${TODAY}" +%s) # # 1. On fait les snapshots sur le ZFS du NAS. -# /sbin/zfs snapshot "tank/${TANK}@${TODAY}" +/sbin/zfs snapshot "tank/${TANK}@${TODAY}" # 2. On envoie les snapshots sur le ZFS de backup. @@ -76,48 +76,48 @@ do done -# # 3. On ne garde que les 15 dernières snapshots ZFS -# LIMIT=$(date -d "${TODAY} -15days" "+%Y-%m-%d") -# LIMIT_EPOCH=$(date -d "${LIMIT}" "+%s") -# -# while true -# do -# -# # On vérifie qu'il existe au moins 15 backups -# COUNT=$(zfs list -t snapshot \ -# | grep -c "tank/${TANK}") -# -# if [ "${COUNT}" -le 15 ] -# then -# echo "Il y a moins de 16 backups. Fin." -# break -# fi -# -# -# # On récupère la plus vieille snapshot -# OLDEST_SNAPSHOT=$(zfs list -t snapshot \ -# | grep "tank/${TANK}" \ -# | cut -d' ' -f1 | cut -d'@' -f2 \ -# | sort | head -n1) -# -# OLDEST_SNAPSHOT_EPOCH=$(date -d "${OLDEST_SNAPSHOT}" "+%s") -# -# # Sanity-check: Si la plus vieille backup est celle d'il y a moins de 15 jours: On sort. -# if [ "${OLDEST_SNAPSHOT_EPOCH}" -ge "${LIMIT_EPOCH}" ] -# then -# echo "La backup locale ${TANK} ${OLDEST_SNAPSHOT} est suffisament récente. Fin." -# break -# fi -# -# # Sinon, on supprime la plus vieille snapshot. -# echo "Suppression de la backup ${TANK} ${OLDEST_SNAPSHOT}." -# -# zfs destroy "tank/${TANK}@${OLDEST_SNAPSHOT}" -# -# if [ $? -ne 0 ] -# then -# echo "La suppression s'est mal déroulée. Arrêt." -# exit 1 -# fi -# -# done +# 3. On ne garde que les 15 dernières snapshots ZFS +LIMIT=$(date -d "${TODAY} -15days" "+%Y-%m-%d") +LIMIT_EPOCH=$(date -d "${LIMIT}" "+%s") + +while true +do + + # On vérifie qu'il existe au moins 15 backups + COUNT=$(zfs list -t snapshot \ + | grep -c "tank/${TANK}") + + if [ "${COUNT}" -le 15 ] + then + echo "Il y a moins de 16 backups. Fin." + break + fi + + + # On récupère la plus vieille snapshot + OLDEST_SNAPSHOT=$(zfs list -t snapshot \ + | grep "tank/${TANK}" \ + | cut -d' ' -f1 | cut -d'@' -f2 \ + | sort | head -n1) + + OLDEST_SNAPSHOT_EPOCH=$(date -d "${OLDEST_SNAPSHOT}" "+%s") + + # Sanity-check: Si la plus vieille backup est celle d'il y a moins de 15 jours: On sort. + if [ "${OLDEST_SNAPSHOT_EPOCH}" -ge "${LIMIT_EPOCH}" ] + then + echo "La backup locale ${TANK} ${OLDEST_SNAPSHOT} est suffisament récente. Fin." + break + fi + + # Sinon, on supprime la plus vieille snapshot. + echo "Suppression de la backup ${TANK} ${OLDEST_SNAPSHOT}." + + zfs destroy "tank/${TANK}@${OLDEST_SNAPSHOT}" + + if [ $? -ne 0 ] + then + echo "La suppression s'est mal déroulée. Arrêt." + exit 1 + fi + +done diff --git a/roles/zfs-backup/handlers/main.yml b/roles/zfs-backup/handlers/main.yml index 60f493a..7e0fd5a 100644 --- a/roles/zfs-backup/handlers/main.yml +++ b/roles/zfs-backup/handlers/main.yml @@ -2,4 +2,10 @@ - name: Run systemd daemon-reload systemd: daemon_reload: true + +- name: Restart and enable ZFS-backup timer + systemd: + name: zfs-backup.timer + state: restarted + enabled: true ... diff --git a/roles/zfs-backup/tasks/main.yml b/roles/zfs-backup/tasks/main.yml index 0d27be0..ce06e4f 100644 --- a/roles/zfs-backup/tasks/main.yml +++ b/roles/zfs-backup/tasks/main.yml @@ -11,22 +11,13 @@ - zfs-backup.timer notify: - Run systemd daemon-reload + - Restart and enable ZFS-backup timer - name: Copie du script - template: - src: zfs-snapshot-nas + copy: + src: files/zfs-snapshot-nas dest: "{{ zfs_backup.scriptpath }}" owner: root group: root mode: u=rx,g=r,o= - -- name: Run systemd deamon-reload - systemd: - daemon_reload: true - -- name: Start and enable ZFS-backup timer - systemd: - name: zfs-backup.timer - state: started - enabled: true ... diff --git a/roles/zfs-backup/templates/zfs-backup.service.j2 b/roles/zfs-backup/templates/zfs-backup.service.j2 index 5bfacdf..cc25d10 100644 --- a/roles/zfs-backup/templates/zfs-backup.service.j2 +++ b/roles/zfs-backup/templates/zfs-backup.service.j2 @@ -1,3 +1,5 @@ +{{ ansible_managed | comment }} + [Unit] Description=Service pour ZFS-backup Wants=network-online.target @@ -7,6 +9,8 @@ After=network-online.target Type=simple User=root +TimeoutStartSec = 7200 + Nice=19 CPUSchedulingPolicy=batch diff --git a/roles/zfs-backup/templates/zfs-backup.timer.j2 b/roles/zfs-backup/templates/zfs-backup.timer.j2 index abb0a1a..255ee3f 100644 --- a/roles/zfs-backup/templates/zfs-backup.timer.j2 +++ b/roles/zfs-backup/templates/zfs-backup.timer.j2 @@ -1,3 +1,5 @@ +{{ ansible_managed | comment }} + [Unit] Description=Timer for ZFS-backup diff --git a/roles/zfs-prune/defaults/main.yml b/roles/zfs-prune/defaults/main.yml new file mode 100644 index 0000000..50b8d41 --- /dev/null +++ b/roles/zfs-prune/defaults/main.yml @@ -0,0 +1,4 @@ +--- +zfs_prune: + scriptpath: /var/zfs-prune-snapshot +... diff --git a/roles/zfs-prune/files/zfs-remove-old-snapshot b/roles/zfs-prune/files/zfs-remove-old-snapshot new file mode 100755 index 0000000..b11c443 --- /dev/null +++ b/roles/zfs-prune/files/zfs-remove-old-snapshot @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +if [ $# -ne 1 ] +then + echo "USAGE: $0 " + exit 1 +fi + +TANK="$1" + +IS_TANK_EXIST=$(zfs list | grep -c "tank/${TANK} ") + +if [ "${IS_TANK_EXIST}" -ne 1 ] +then + echo "${TANK} n'existe pas. Arrêt." + exit 1 +fi + + +TODAY=$(date "+%Y-%m-%d") + +# On ne garde que les 365 dernières snapshots ZFS +LIMIT=$(date -d "${TODAY} -1year" "+%Y-%m-%d") +LIMIT_EPOCH=$(date -d "${LIMIT}" "+%s") + +while true +do + + # On vérifie qu'il existe au moins 365 backups + COUNT=$(zfs list -t snapshot \ + | grep -c "tank/${TANK}") + + if [ "${COUNT}" -le 365 ] + then + echo "Il y a moins de 366 backups. Fin." + break + fi + + # On récupère la plus vieille snapshot + OLDEST_SNAPSHOT=$(zfs list -t snapshot \ + | grep "tank/${TANK}" \ + | cut -d' ' -f1 | cut -d'@' -f2 \ + | sort | head -n1) + + OLDEST_SNAPSHOT_EPOCH=$(date -d "${OLDEST_SNAPSHOT}" "+%s") + + # Sanity-check: Si la plus vieille backup est celle d'il y a moins d'un an: On sort. + if [ "${OLDEST_SNAPSHOT_EPOCH}" -ge "${LIMIT_EPOCH}" ] + then + echo "La backup locale ${TANK} ${OLDEST_SNAPSHOT} est suffisament récente. Fin." + break + fi + + # Sinon, on supprime la plus vieille snapshot. + echo "Suppression de la backup ${TANK} ${OLDEST_SNAPSHOT}." + sleep 2 + + zfs destroy "tank/${TANK}@${OLDEST_SNAPSHOT}" + + if [ $? -ne 0 ] + then + echo "La suppression s'est mal déroulée. Arrêt." + exit 1 + fi + +done diff --git a/roles/zfs-prune/handlers/main.yml b/roles/zfs-prune/handlers/main.yml new file mode 100644 index 0000000..e3c0834 --- /dev/null +++ b/roles/zfs-prune/handlers/main.yml @@ -0,0 +1,11 @@ +--- +- name: Run systemd daemon-reload + systemd: + daemon_reload: true + +- name: Restart and enable ZFS-prune timer + systemd: + name: zfs-prune.timer + state: restarted + enabled: true +... diff --git a/roles/zfs-prune/tasks/main.yml b/roles/zfs-prune/tasks/main.yml new file mode 100644 index 0000000..eab8ff9 --- /dev/null +++ b/roles/zfs-prune/tasks/main.yml @@ -0,0 +1,23 @@ +--- +- name: Copy files for zfs-prune + template: + src: "{{ item }}.j2" + dest: /etc/systemd/system/{{ item }} + owner: root + group: root + mode: u=rw,g=r,o= + loop: + - zfs-prune.service + - zfs-prune.timer + notify: + - Run systemd daemon-reload + - Restart and enable ZFS-prune timer + +- name: Copie du script + copy: + src: files/zfs-remove-old-snapshot + dest: "{{ zfs_prune.scriptpath }}" + owner: root + group: root + mode: u=rx,g=r,o= +... diff --git a/roles/zfs-prune/templates/zfs-prune.service.j2 b/roles/zfs-prune/templates/zfs-prune.service.j2 new file mode 100644 index 0000000..acb9def --- /dev/null +++ b/roles/zfs-prune/templates/zfs-prune.service.j2 @@ -0,0 +1,23 @@ +{{ ansible_managed | comment }} + +[Unit] +Description=Service pour ZFS-backup +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +User=root + +TimeoutStartSec = 7200 + +Nice=19 +CPUSchedulingPolicy=batch + +Restart=no + +LogRateLimitIntervalSec=0 + +ExecStartPre=/usr/bin/bash {{ zfs_prune.scriptpath }} nextcloud_backup +ExecStart=/usr/bin/bash {{ zfs_prune.scriptpath }} gitea_backup + diff --git a/roles/zfs-prune/templates/zfs-prune.timer.j2 b/roles/zfs-prune/templates/zfs-prune.timer.j2 new file mode 100644 index 0000000..d2c8698 --- /dev/null +++ b/roles/zfs-prune/templates/zfs-prune.timer.j2 @@ -0,0 +1,11 @@ +{{ ansible_managed | comment }} + +[Unit] +Description=Timer for ZFS-prune + +[Timer] +OnCalendar=daily + +[Install] +WantedBy=timers.target + diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..83a7687 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ pkgs ? import {} }: + pkgs.mkShell { + buildInputs = with pkgs.buildPackages; [ + ansible_2_16 + python313Packages.jinja2 + python313Packages.requests + python313Packages.pysocks + ]; + LANG="C.UTF-8"; +} From 44b36b16c5baf453a2b5412b9ab262d10d728481 Mon Sep 17 00:00:00 2001 From: korenstin Date: Fri, 8 Aug 2025 01:23:34 +0200 Subject: [PATCH 04/10] =?UTF-8?q?On=20enl=C3=A8ve=20radius-fleming=20de=20?= =?UTF-8?q?la=20surveillance,=20il=20est=20down=20de=20toute=20fa=C3=A7on?= =?UTF-8?q?=20(cf=20df420920fc44ea63666df4a5ce758286c9c42935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hosts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts b/hosts index 3c257c4..531d419 100644 --- a/hosts +++ b/hosts @@ -193,7 +193,7 @@ dns-fleming.adm.auro.re dns-fleming-backup.adm.auro.re prometheus-fleming.adm.auro.re ns-1.auro.re -radius-fleming.adm.auro.re +#radius-fleming.adm.auro.re radius-fleming-backup.adm.auro.re unifi-fleming.adm.auro.re routeur-fleming.adm.auro.re From 9c3dc75323d22e96f6c973989752f9606c0aa612 Mon Sep 17 00:00:00 2001 From: korenstin Date: Fri, 8 Aug 2025 10:45:09 +0200 Subject: [PATCH 05/10] Remove pacaterie --- hosts | 71 ----------------------------------------------------------- 1 file changed, 71 deletions(-) diff --git a/hosts b/hosts index 531d419..6dc745b 100644 --- a/hosts +++ b/hosts @@ -294,59 +294,6 @@ fl-3-2.borne.auro.re fl-4-1.borne.auro.re fl-4-2.borne.auro.re -############################################################################### -# Pacaterie - -[pacaterie_pve] -mordred.adm.auro.re -titan.adm.auro.re - -[pacaterie_vm] -ldap-replica-pacaterie.adm.auro.re -dhcp-pacaterie.adm.auro.re -dhcp-pacaterie-backup.adm.auro.re -dns-pacaterie.adm.auro.re -dns-pacaterie-backup.adm.auro.re -prometheus-pacaterie.adm.auro.re -#prometheus-pacaterie-fo.adm.auro.re -radius-pacaterie.adm.auro.re -radius-pacaterie-backup.adm.auro.re -unifi-pacaterie.adm.auro.re -routeur-pacaterie.adm.auro.re -routeur-pacaterie-backup.adm.auro.re - -[pacaterie_unifi] -pc-1-1.borne.auro.re -pn-0-1.borne.auro.re -pn-0-2.borne.auro.re -pn-0-3.borne.auro.re -pn-1-1.borne.auro.re -pn-1-2.borne.auro.re -pn-1-3.borne.auro.re -pn-2-1.borne.auro.re -pn-2-2.borne.auro.re -pn-2-3.borne.auro.re -pn-3-1.borne.auro.re -pn-3-2.borne.auro.re -pn-3-3.borne.auro.re -pn-4-1.borne.auro.re -pn-4-2.borne.auro.re -pn-4-3.borne.auro.re -ps-0-1.borne.auro.re -ps-0-2.borne.auro.re -ps-0-3.borne.auro.re -ps-1-1.borne.auro.re -ps-1-2.borne.auro.re -ps-1-3.borne.auro.re -ps-2-1.borne.auro.re -ps-2-2.borne.auro.re -ps-2-3.borne.auro.re -ps-3-1.borne.auro.re -ps-3-2.borne.auro.re -ps-3-3.borne.auro.re -ps-4-1.borne.auro.re -ps-4-2.borne.auro.re -ps-4-3.borne.auro.re ############################################################################### # Emilie du Chatelet @@ -550,12 +497,6 @@ fleming_pve fleming_vm fleming_unifi -# everything at pacaterie -[pacaterie:children] -pacaterie_pve -pacaterie_vm -pacaterie_unifi - # everything at edc [edc:children] edc_server @@ -586,7 +527,6 @@ ovh_container #[vm:children] #ovh_vm #fleming_vm -#pacaterie_vm #edc_vm #gs_vm #rives_vm @@ -600,7 +540,6 @@ edc_server #[pve:children] #ovh_pve #fleming_pve -#pacaterie_pve #edc_pve #gs_pve #rives_pve @@ -611,7 +550,6 @@ gs_unifi edc_unifi fleming_unifi rives_unifi -pacaterie_unifi ############################################################################### # Groups by service @@ -619,8 +557,6 @@ pacaterie_unifi [routeur] routeur-fleming.adm.auro.re routeur-fleming-backup.adm.auro.re -routeur-pacaterie.adm.auro.re -routeur-pacaterie-backup.adm.auro.re routeur-edc.adm.auro.re routeur-edc-backup.adm.auro.re routeur-gs.adm.auro.re @@ -632,7 +568,6 @@ routeur-aurore-backup.adm.auro.re [ldap_replica:children] ldap_replica_fleming -ldap_replica_pacaterie ldap_replica_edc ldap_replica_gs ldap_replica_ovh @@ -641,9 +576,6 @@ ldap_replica_rives [ldap_replica_fleming] ldap-replica-fleming.adm.auro.re -[ldap_replica_pacaterie] -ldap-replica-pacaterie.adm.auro.re - [ldap_replica_edc] ldap-replica-edc.adm.auro.re @@ -685,8 +617,6 @@ radius-edc.adm.auro.re radius-edc-backup.adm.auro.re radius-gs.adm.auro.re radius-gs-backup.adm.auro.re -radius-pacaterie.adm.auro.re -radius-pacaterie-backup.adm.auro.re radius-rives.adm.auro.re radius-rives-backup.adm.auro.re @@ -696,6 +626,5 @@ prometheus-aurore.adm.auro.re prometheus-rives.adm.auro.re prometheus-gs.adm.auro.re prometheus-edc.adm.auro.re -prometheus-pacaterie.adm.auro.re prometheus-fleming.adm.auro.re prometheus-federate.adm.auro.re From ec13538cb73de60b0d22aaae0836abb60db7fe12 Mon Sep 17 00:00:00 2001 From: korenstin Date: Fri, 8 Aug 2025 19:50:09 +0200 Subject: [PATCH 06/10] Switch: creation of a module to configure the switchs with Ansible. --- README.md | 143 ++++++++- ansible.cfg | 2 + filter_plugins/switch_range.py | 38 +++ group_vars/switch.yml | 12 + host_vars/switch-dev.yml | 52 ++++ hosts | 3 + library/switch_config.py | 390 ++++++++++++++++++++++++ playbooks/switch.yml | 17 ++ roles/switch-ports/tasks/main.yml | 48 +++ roles/switch-system/tasks/main.yml | 67 ++++ roles/switch-vlans-ports/tasks/main.yml | 52 ++++ roles/switch-vlans/tasks/main.yml | 80 +++++ shell.nix | 1 + 13 files changed, 901 insertions(+), 4 deletions(-) create mode 100644 filter_plugins/switch_range.py create mode 100644 group_vars/switch.yml create mode 100644 host_vars/switch-dev.yml create mode 100644 library/switch_config.py create mode 100755 playbooks/switch.yml create mode 100644 roles/switch-ports/tasks/main.yml create mode 100644 roles/switch-system/tasks/main.yml create mode 100644 roles/switch-vlans-ports/tasks/main.yml create mode 100644 roles/switch-vlans/tasks/main.yml diff --git a/README.md b/README.md index 5f2da81..8ca927f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Il contient la définition de chaque machine et le regroupement. Quand on regroupe avec un `:children` en réalité on groupe des groupes. Chaque machine est annoncée avec son hostname. Il faut pouvoir SSH sur cette machine -avec ce hostname, car c'est ce qu'Ansible fera. +avec ce hostname, car c'est ce qu'Ansible fera (sauf pour les switchs, voir plus bas). **Playbook** : c'est une politique de déploiement. Il contient les associations des rôles avec les machines. @@ -41,9 +41,9 @@ action. Elle est associée à un module Ansible. un fichier avec le module `lineinfile`, copier une template avec le module `template`… Une tâche peut avoir des paramètres supplémentaires pour la réessayer quand elle plante, -récupérer son résultat dans une varible, mettre une boucle dessus, mettre des conditions… +récupérer son résultat dans une variable, mettre une boucle dessus, mettre des conditions… -N'oubliez pas d'aller lire l'excellent documentation de RedHat sur tous les modules +N'oubliez pas d'aller lire l'excellente documentation de RedHat sur tous les modules d'Ansible ! ### Gestion des groupes de machines @@ -83,7 +83,7 @@ ansible proxy.adm.auro.re -m setup --ask-vault-pass ### Configurer la connexion au vlan adm Envoyer son agent SSH peut être dangereux -([source](https://heipei.io/2015/02/26/SSH-Agent-Forwarding-considered-harmful/)). +([source](https://heipei.github.io/2015/02/26/SSH-Agent-Forwarding-considered-harmful/)). On va utiliser plutôt `ProxyJump`. Dans la configuration SSH : @@ -125,6 +125,10 @@ for ip in `cat hosts|grep .adm.auro.re`; do done ``` +> Remarque : +> +> L'utilisation d'un certificat permet d'éviter d'avoir à ajouter sa clé ssh +> sur les serveurs. ### Passage à Ansible 2.10 (release: 30 juillet) @@ -144,3 +148,134 @@ workaround est le suivant : Notez l'espace au début pour ne pas log la commande dans votre historique shell. + + +## Configuration des switchs depuis Ansible + +Afin d'acquérir de l'indépendance vis-à-vis de re2o, un module permettant de +configurer les switchs depuis Ansible a été créé. Il utilise l'api rest des +switchs afin de récupérer et appliquer la configuration voulu. + +### Prérequis + +Pour utiliser le module, il faut d'abord annoncer à Ansible qu'il ne faut pas +effectuer de connexion ssh et de ne pas récupérer les faits. Cela se fait à +l'aide des variables `connection: httpapi` et `gather_facts: false`. Ensuite, +l'infrasutructue actuelle de Aurore nécéssite l'utilisation d'un proxy. Pour +cela, il suffit d'éxecuter la commande : + +```bash +ssh -D 3000 switchs-manager.adm.auro.re +``` + +et d'annoncer l'utilisation du proxy dans la configuration en exportant la +variable d'environnement `HTTP_PROXY=socks5://localhost:3000` et en +configurant la variable du module `use_proxy: true`. + +Exemple : + +```yaml +environment: + HTTP_PROXY: "socks5://localhost:3000" +tasks: + - name: vlans + switch_config: + username: **** + password: **** + port: 80 + host: 192.168.1.42 + use_proxy: true + config: + path: vlans/42 + data: + name: VLAN42 + vlan_id: 42 + status: VS_PORT_BASED + type: VT_STATIC +``` + +Le module est alors utilisable, il ne reste plus qu'à le configurer. + +### Écrire la configuration + +Le module se veut assez libre. Ainsi, l'ensemble de la requête doit être écrite +dans les `tasks`. Voici un exemple pour configurer un vlan : + +```yaml +tasks: + - name: vlans + switch_config: + username: **** + password: **** + port: 80 + host: 192.168.1.42 + config: + path: vlans/42 + data: + name: VLAN42 + vlan_id: 42 + status: VS_PORT_BASED + type: VT_STATIC +``` + +Le `path` correspond à l'url de l'objet que l'on souhaite éditer et `data` +correspond aux données qui seront envoyées dans une requête `PUT` (au format +`json`). Cependant, la configuration d'un vlan peut nécessité de le créer. +Pour remédier à ce problème, il est possible d'utiliser la syntaxe suivante : + +```yaml + +tasks: + - name: vlans + switch_config: + username: **** + password: **** + port: 80 + host: 192.168.1.42 + config: + path: vlans + create_method: POST + subpath: + - path: 42 + data: + name: VLAN42 + vlan_id: 42 + status: VS_PORT_BASED + type: VT_STATIC +``` + +Le variable `create_method` correspond au type de la requête pour effectuer une +action de création de l'objet. Il s'agit généralement de `POST`. Dans le cas +où la variable n'est pas définit, la création sera désactivée et ainsi, si +l'url indiqué dans les `subpath` n'existe pas, alors la configuration échouera. +Par conséquent, si le vlan 42 a besoin d'être créé, une requête `POST` sera +effectué sur l'url `vlans` avec les données dans `data`. + +Il est également possible d'éxecuter une action de suppression d'un vlan à l'aide +de la variable `delete` : + +```yaml + tasks: + - name: vlans + switch_config: + username: **** + password: **** + port: 80 + host: 192.168.1.42 + config: + path: vlans/42 + delete: true +``` + +Si la variable `delete` est activée, alors une requête `DELETE` sera envoyée +sur l'url indiquée. Pour vérifier si la suppression est déjà effective avant +l'éxecution, le module vérifiera si un `GET` sur l'url retourne une 404. + +> Remarque : +> +> Si les variables `delete` et `data` sont définies (dont `delete` à `true`), +> alors il en résultera une action de suppression malgrés tout. + +Puisque `subpath` est une liste, il est possible de configurer plusieurs requête +en même temps. Cela à l'avantage d'effectuer toutes les modifications à la suite +(sans avoir à se connecter plusieurs sur l'api). diff --git a/ansible.cfg b/ansible.cfg index 805a0b1..b591cde 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,4 +1,6 @@ [defaults] +jinja2_native = true + ask_vault_pass = True roles_path = ./roles retry_files_enabled = False diff --git a/filter_plugins/switch_range.py b/filter_plugins/switch_range.py new file mode 100644 index 0000000..6eb0b37 --- /dev/null +++ b/filter_plugins/switch_range.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +class FilterModule(object): + def filters(self): + return { + 'range2list': self.range2list, + } + + def range2list(self, port_range): + """ + Convert a range into list + + Exemple: + ``` + >>> FilterModule.range2list("1-10,42") + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 42] + ```` + """ + port_range = port_range.replace(" ", "").split(",") + ports = [] + for r in port_range: + if "-" in r: + try: + a, b = r.split("-") + except: + raise Exception("A range must contain 2 values") + try: + a = int(a) + b = int(b) + except: + raise TypeError("A range must contain integer") + for n in range(a, b+1): + ports.append(n) + else: + try: + ports.append(int(r)) + except: + raise TypeError("Value must be integer") + return list(set(ports)) diff --git a/group_vars/switch.yml b/group_vars/switch.yml new file mode 100644 index 0000000..74d8bdf --- /dev/null +++ b/group_vars/switch.yml @@ -0,0 +1,12 @@ +--- +glob_switch: + loop_protect: + port_disable_timer_in_seconds: 30 + transmit_interval_in_seconds: 3 + sntp: + operation_mode: SNTP_UNICAST_MODE + poll_interval: 720 + servers: + - ip: 10.130.0.15 + priority: 1 +... diff --git a/host_vars/switch-dev.yml b/host_vars/switch-dev.yml new file mode 100644 index 0000000..eda335f --- /dev/null +++ b/host_vars/switch-dev.yml @@ -0,0 +1,52 @@ +--- +switch_vars: + name: switch-dev + location: Emilie du Chatelet + host: 10.130.4.199 + port: 80 + username: CHANGE + password: ME + vlans: + - id: 300 + name: "VLAN_TEST_300" + ipaddresses: + - mode: IAAM_DHCP + tagged: "{{ '10-12' | range2list }}" + - id: 301 + name: "VLAN_TEST_301" + ipaddresses: + - mode: IAAM_STATIC + ip: 10.203.4.199 + mask: 255.255.0.0 + - mode: IAAM_STATIC + ip: 10.204.4.199 + delete: true + tagged: [10, 11] + untagged: [12] + - id: 302 + name: "VLAN_TEST_302" + remove_ports: "{{ '10-12,13' | range2list }}" + delete_vlans: + - 400 + ports: + - id: 10 + name: "PORT_TEST_10" + enabled: false + loop_protect: true + lldp: true + - id: 11 + name: "PORT_TEST_11" + loop_protect: false + lldp: true + - id: 12 + name: "PORT_TEST_12" + loop_protect: true + - id: 13 + name: "PORT_TEST_13" + - id: 14 + name: "PORT_TEST_14" + loop_protect: true + - id: 15 + name: "PORT_TEST_15" + loop_protect: true +... diff --git a/hosts b/hosts index 6dc745b..37bbd6f 100644 --- a/hosts +++ b/hosts @@ -1,5 +1,8 @@ # Aurore servers inventory +[switch] +switch-dev + [vm_test] mx.test.infra.auro.re diff --git a/library/switch_config.py b/library/switch_config.py new file mode 100644 index 0000000..07b0fc6 --- /dev/null +++ b/library/switch_config.py @@ -0,0 +1,390 @@ +#!/usr/bin/python + +DOCUMENTATION = """ +--- +module: Switch + +short_description: Allow the setup of switches using rest API + +description: Allow the setup of switches using rest API + +options: + config: + description: configuration to send to the switch. + required: true + type: dict + host: + description: host of switch. + required: false + type: str + password: + description: password of the user. + required: true + type: str + port: + description: port of rest api. + required: false + type: int + use_proxy: + description: + Use a proxy to communicate with the switch. + HTTP_PROXY or ALL_PROXY must be set. + required: false + type: bool + username: + description: username of the rest API. + required: true + type: str + version: + description: version of the rest API. + required: false + type: str +""" + +EXAMPLES = """ +- name: Setup switch name + switch_config: + username: test + password: 1234 + host: 192.168.1.1 + port: 80 + version: v8 + config: + path: system + data: + name: "SwitchName" + +- name: Setup vlans + switch_config: + username: test + password: 1234 + host: 192.168.1.1 + port: 80 + config: + path: vlans + create_method: POST + subpath: + - path: 42 + data: + name: "TheAnswer" + vlan_id: 42 + status: VS_PORT_BASED + type: VT_STATIC +""" + +from ansible.module_utils.basic import AnsibleModule + +import json +import os +import requests + +class SwitchApi: + def __init__(self, port, host, use_proxy, api="v1"): + self.headers = {'Content-Type': 'application/json'} + self.url_base = f"http://{host}:{port}/rest/{api}" + + self.proxies = None + if use_proxy: + http_proxy = os.getenv("HTTP_PROXY") + all_proxy = os.getenv("ALL_PROXY") + + if http_proxy != "": + self.proxies = {'http': http_proxy} + elif all_proxy != "": + self.proxies = {'http': all_proxy} + + def login(self, username, password): + """ + Log in to the rest api. + Return True if the connection has succeeded and False otherwise. + """ + data = {"userName": username, "password": password} + response = self.post("/login-sessions", data = json.dumps(data),) + + if response.status_code != 201: + return False + + data = response.json() + if not 'cookie' in data: + return False + + self.headers['cookie'] = data['cookie'] + return True + + def logout(self): + """ + Log out of the rest api. + Return True if connection has succeeded and False otherwise + """ + response = self.delete("/login-sessions") + if response.status_code != 204: + return False + self.headers.pop('cookie') + return True + + def post(self, url, data = None): + kwargs = { + "headers": self.headers + } + if data is not None: + kwargs["data"] = data + if self.proxies is not None: + kwargs["proxies"] = self.proxies + + return requests.post(self.url_base + url, **kwargs) + + def get(self, url, data = ""): + kwargs = { + "headers": self.headers + } + if data is not None: + kwargs["data"] = data + if self.proxies is not None: + kwargs["proxies"] = self.proxies + + return requests.get(self.url_base + url, **kwargs) + + def delete(self, url, data = ""): + kwargs = { + "headers": self.headers + } + if data is not None: + kwargs["data"] = data + if self.proxies is not None: + kwargs["proxies"] = self.proxies + + return requests.delete(self.url_base + url, **kwargs) + + def put(self, url, data = ""): + kwargs = { + "headers": self.headers + } + if data is not None: + kwargs["data"] = data + if self.proxies is not None: + kwargs["proxies"] = self.proxies + + return requests.put(self.url_base + url, **kwargs) + + +def required_modification(current_conf, modification): + for k, v in modification.items(): + if not k in current_conf: + return True + if current_conf[k] != v: + return True + return False + + +def configure(module, config, api, current_path="", create_method=None): + path = "/" + str(config["path"]) + url = current_path + path + check_mode = module.check_mode + changed = False + before = {"path": path} + after = {"path": path} + + if not "path" in config: + api.logout() + raise Exception("A path must be specified.") + + # If removing configuration + if "delete" in config and config["delete"]: + # Get the configuration + response = api.get(url) + if response.status_code == 404: + before["delete"] = True + elif response.status_code in (200, 201, 202, 203, 204): + before["data"] = response.json() + else: + api.logout() + raise Exception( + "Failed to check the old configuration:", + f"Url: {response.url}", + f"Status code: {response.status_code}", + f"Response: {response.text}", + ) + + # If required, delete + if not "delete" in before and not check_mode: + response = api.delete(url) + if response.status_code >= 400: + api.logout() + raise Exception( + "Failed to delete:", + f"Url: {response.url}", + f"Status code: {response.status_code}", + f"Response: {response.text}", + ) + else: + # Verify that everything is ok + response = api.get(url) + if response.status_code == 404: + changed = True + after["delete"] = True + elif response.status_code in (200, 201, 202, 203, 204): + after["data"] = response.json() + after["delete"] = False + else: + api.logout() + raise Exception( + "Failed to check the configuration after delete:", + f"Url: {response.url}", + f"Status code: {response.status_code}", + f"Response: {response.text}", + ) + elif not "delete" in before: + after["delete"] = True + changed = True + else: + after["delete"] = True + + # If create or edit + elif "data" in config and type(config["data"]) is dict: + # Get the configuration + response = api.get(url) + new_data = {} + if response.status_code == 404: + before["delete"] = True + elif response.status_code in (200, 201, 202, 203, 204): + before["data"] = response.json() + new_data = before["data"].copy() + else: + api.logout() + raise Exception( + "Failed to check the old configuration:", + f"Url: {response.url}", + f"Status code: {response.status_code}", + f"Response: {response.text}", + ) + + # If required, modify + if "delete" in before and not check_mode: + # Create + if create_method == "POST": + response = api.post(current_path, json.dumps(config["data"])) + elif create_method == "PUT": + response = api.put(current_path, json.dumps(config["data"])) + else: + api.logout() + raise Exception( + "Failed to create:", + "Variable create_method must be set to PUT or POST", + ) + if response.status_code >= 400: + api.logout() + raise Exception( + "Failed to create:", + f"Url: {response.url}", + f"Status code: {response.status_code}", + f"Data: {config["data"]}", + f"Response: {response.text}", + ) + after["data"] = response.json() + changed = True + elif not check_mode: + # Edit + if required_modification(before["data"], config["data"]): + response = api.put(url, json.dumps(config["data"])) + if response.status_code >= 400: + api.logout() + raise Exception( + "Failed to edit:", + f"Url: {response.url}", + f"Status code : {response.status_code}", + f"Data: {config["data"]}", + f"Response: {response.text}", + ) + changed = True + after["data"] = response.json() + else: + after["data"] = before["data"].copy() + else: + if "delete" in before and create_method is None: + api.logout() + raise Exception( + "Failed to create:", + "Variable create_method must be set to PUT or POST", + ) + new_data.update(config["data"]) + after["data"] = new_data + changed = changed or (not "data" in before) or after["data"] != before["data"] + + # Configure the subpaths + if "subpath" in config and type(config["subpath"]) is list: + create_method = None + if "create_method" in config: + create_method = config["create_method"] + before["subpath"] = [] + after["subpath"] = [] + for subconf in config["subpath"]: + response = configure( + module, subconf, + api, + current_path=url, + create_method=create_method + ) + changed = changed or response["changed"] + before["subpath"].append(response["diff"]["before"]) + after["subpath"].append(response["diff"]["after"]) + + return { + "changed": changed, + "diff": {"after": after, "before": before} + } + +def run_module(): + module_args = dict( + config=dict(type='dict', required=True), + username=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + port=dict(type='int', required=True), + host=dict(type='str', required=True), + version=dict(type='str', required=False, default='v1'), + use_proxy=dict(type='bool', required=False, default=False), + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + result = { + "changed": False, + } + + # api connection + api = SwitchApi( + module.params["port"], + module.params["host"], + module.params["use_proxy"], + api = module.params["version"] + ) + login_success = api.login( + module.params["username"], + module.params["password"], + ) + if not login_success: + module.fail_json(msg='login failed', **result) + return + + try: + response = configure(module, module.params["config"], api) + except Exception as msg: + module.fail_json(msg="\n".join(msg.args), **result) + return + api.logout() + result.update(response) + + if module.check_mode: + module.exit_json(**result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/playbooks/switch.yml b/playbooks/switch.yml new file mode 100755 index 0000000..ccd1b19 --- /dev/null +++ b/playbooks/switch.yml @@ -0,0 +1,17 @@ +#!/usr/bin/env ansible-playbook +--- +- hosts: + - switch + connection: httpapi + gather_facts: false + environment: + HTTP_PROXY: "socks5://localhost:3000" + vars: + switch: + use_proxy: true + roles: + - switch-system + - switch-vlans + - switch-ports + - switch-vlans-ports +... diff --git a/roles/switch-ports/tasks/main.yml b/roles/switch-ports/tasks/main.yml new file mode 100644 index 0000000..b535ddd --- /dev/null +++ b/roles/switch-ports/tasks/main.yml @@ -0,0 +1,48 @@ +--- +- name: Configure ports + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + config: + path: ports + subpath: + - path: "{{ item.id }}" + data: + name: "{{ item.name }}" + is_port_enabled: "{{ item.enabled | default(true) }}" + loop: "{{ switch_vars.ports }}" + +- name: Configure lldp + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + config: + path: lldp/local-port + subpath: + - path: "{{ item.id }}" + data: + port_id: "{{ item.id | string }}" + admin_status: "{{ 'LPAS_TX_AND_RX' if item.lldp is defined and item.lldp else 'LPAS_DISABLED' }}" + loop: "{{ switch_vars.ports }}" + +- name: Configure loop-protect + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: "loop_protect/ports/{{ item.id }}" + data: + port_id: "{{ item.id | string }}" + is_loop_protection_enabled: "{{ item.loop_protect | default(False) }}" + loop: "{{ switch_vars.ports }}" +... diff --git a/roles/switch-system/tasks/main.yml b/roles/switch-system/tasks/main.yml new file mode 100644 index 0000000..36e0904 --- /dev/null +++ b/roles/switch-system/tasks/main.yml @@ -0,0 +1,67 @@ +--- +- name: Configure switch + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + config: + path: system + data: + name: "{{ switch_vars.name | default('') }}" + location: "{{ switch_vars.location | default('') }}" + contact: "{{ switch_vars.contact | default('')}}" + +- name: Configure sntp + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: system/sntp + data: + sntp_client_operation_mode: "{{ glob_switch.sntp.operation_mode }}" + sntp_config_poll_interval: "{{ glob_switch.sntp.poll_interval }}" + +- name: Configure sntp servers + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: system/sntp_server + create_method: POST + subpath: + path: "{{ item.priority }}-{{ item.ip }}" + delete: "{{ item.delete | default(False) }}" + data: + sntp_servers: + - sntp_server_address: + version: "{{ item.ip_version | default('IAV_IP_V4') }}" + octets: "{{ item.ip }}" + sntp_server_priority: "{{ item.priority }}" + sntp_server_version: "{{ item.version | default(4) }}" + sntp_server_is_oobm: "{{ item.is_oobm | default(None) }}" + loop: "{{ glob_switch.sntp.servers }}" + +- name: Configure loop-protect + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: loop_protect + data: + port_disable_timer_in_seconds: "{{ glob_switch.loop_protect.port_disable_timer_in_seconds }}" + transmit_interval_in_seconds: "{{ glob_switch.loop_protect.transmit_interval_in_seconds }}" +... diff --git a/roles/switch-vlans-ports/tasks/main.yml b/roles/switch-vlans-ports/tasks/main.yml new file mode 100644 index 0000000..8322908 --- /dev/null +++ b/roles/switch-vlans-ports/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: Configure tagged vlans + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v1 + config: + path: vlans-ports + create_method: POST + subpath: + - path: "{{ item.0.id }}-{{ item.1 }}" + data: + vlan_id: "{{ item.0.id }}" + port_id: "{{ item.1 | string }}" + port_mode: POM_TAGGED_STATIC + loop: "{{ switch_vars.vlans | subelements('tagged', skip_missing=True) }}" + +- name: Configure untagged vlans + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + config: + path: vlans-ports + create_method: POST + subpath: + - path: "{{ item.0.id }}-{{ item.1 }}" + data: + vlan_id: "{{ item.0.id }}" + port_id: "{{ item.1 | string }}" + port_mode: POM_UNTAGGED + loop: "{{ switch_vars.vlans | subelements('untagged', skip_missing=True) }}" + +- name: Remove vlans-ports + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + config: + path: vlans-ports + subpath: + - path: "{{ item.0.id }}-{{ item.1 }}" + delete: true + loop: "{{ switch_vars.vlans | subelements('remove_ports', skip_missing=True) }}" +... diff --git a/roles/switch-vlans/tasks/main.yml b/roles/switch-vlans/tasks/main.yml new file mode 100644 index 0000000..1c84593 --- /dev/null +++ b/roles/switch-vlans/tasks/main.yml @@ -0,0 +1,80 @@ +--- +- name: Configure vlans + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: vlans + create_method: POST + subpath: + - path: "{{ item.id }}" + data: + name: "{{ item.name }}" + vlan_id: "{{ item.id }}" + status: VS_PORT_BASED + type: VT_STATIC + loop: "{{ switch_vars.vlans }}" + +- name: Remove vlans + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: vlans + subpath: + - path: "{{ item }}" + delete: true + loop: "{{ switch_vars.delete_vlans }}" + +- name: Configure IP + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: "vlans/{{ item.0.id }}/ipaddresses" + create_method: POST + subpath: + - path: "{{ item.1.mode }}-{{ item.1.ip }}" + delete: "{{ item.1.delete | default(False) }}" + data: + ip_address_mode: "{{ item.1.mode }}" + vlan_id: "{{ item.0.id }}" + ip_address: + version: "{{ item.1.version | default('IAV_IP_V4') }}" + octets: "{{ item.1.ip }}" + ip_mask: + version: "{{ item.1.version | default('IAV_IP_V4') }}" + octets: "{{ item.1.mask | default('255.255.255.0') }}" + loop: "{{ switch_vars.vlans | subelements('ipaddresses', skip_missing=True) | selectattr('1.mode', '==', 'IAAM_STATIC') }}" + +- name: Configure vlan without IP + switch_config: + username: "{{ switch_vars.username }}" + password: "{{ switch_vars.password }}" + port: "{{ switch_vars.port }}" + host: "{{ switch_vars.host }}" + use_proxy: "{{ switch.use_proxy }}" + version: v8 + config: + path: "vlans/{{ item.0.id }}/ipaddresses" + create_method: POST + subpath: + - path: "{{ item.1.mode }}-0.0.0.0" + delete: "{{ item.1.delete | default(False) }}" + data: + ip_address_mode: "{{ item.1.mode }}" + vlan_id: "{{ item.0.id }}" + loop: "{{ switch_vars.vlans | subelements('ipaddresses', skip_missing=True) | rejectattr('1.mode', '==', 'IAAM_STATIC') }}" +... diff --git a/shell.nix b/shell.nix index 83a7687..c04a11d 100644 --- a/shell.nix +++ b/shell.nix @@ -5,6 +5,7 @@ python313Packages.jinja2 python313Packages.requests python313Packages.pysocks + python313Packages.dns ]; LANG="C.UTF-8"; } From b74cf3faea4b76f5f7138681b52db78651b249fd Mon Sep 17 00:00:00 2001 From: korenstin Date: Thu, 14 Aug 2025 21:37:50 +0200 Subject: [PATCH 07/10] Add example and typo --- README.md | 90 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 8ca927f..a8a0909 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -[![Linter Status](https://drone.auro.re/api/badges/Aurore/ansible/status.svg)](https://drone.auro.re/Aurore/ansible) - # Recettes Ansible d'Aurore Dépendances requises : - * Ansible 2.9 ou plus récent. +* Ansible 2.9 ou plus récent. ## Ansible 101 @@ -15,8 +13,9 @@ Il contient la définition de chaque machine et le regroupement. Quand on regroupe avec un `:children` en réalité on groupe des groupes. -Chaque machine est annoncée avec son hostname. Il faut pouvoir SSH sur cette machine -avec ce hostname, car c'est ce qu'Ansible fera (sauf pour les switchs, voir plus bas). +Chaque machine est annoncée avec son hostname. Il faut pouvoir SSH sur cette +machine avec ce hostname, car c'est ce qu'Ansible fera (sauf pour les switchs, +voir plus bas). **Playbook** : c'est une politique de déploiement. Il contient les associations des rôles avec les machines. @@ -37,11 +36,13 @@ déployer un serveur prometheus, déployer une node prometheus… **Tâche** : un rôle est composé de tâches. Une tâche effectue une et une seule action. Elle est associée à un module Ansible. -*Exemples de tâche* : installer un paquet avec le module `apt`, ajouter une ligne dans -un fichier avec le module `lineinfile`, copier une template avec le module `template`… +*Exemples de tâche* : installer un paquet avec le module `apt`, ajouter une +ligne dans un fichier avec le module `lineinfile`, copier une template avec le +module `template`… -Une tâche peut avoir des paramètres supplémentaires pour la réessayer quand elle plante, -récupérer son résultat dans une variable, mettre une boucle dessus, mettre des conditions… +Une tâche peut avoir des paramètres supplémentaires pour la réessayer quand +elle plante, récupérer son résultat dans une variable, mettre une boucle +dessus, mettre des conditions… N'oubliez pas d'aller lire l'excellente documentation de RedHat sur tous les modules d'Ansible ! @@ -50,18 +51,27 @@ d'Ansible ! Pour la liste complète, je vous invite à lire le fichier `hosts`. - * pour tester les versions de Debian, +Exemple : - ```YAML - ansible_lsb.codename == 'stretch' - ``` +```yaml +[fleming_vm] +dhcp-fleming.adm.auro.re +dns-fleming.adm.auro.re +prometheus-fleming.adm.auro.re +routeur-fleming.adm.auro.re - * pour tester si c'est un CPU Intel x86_64, +[fleming_pve] +pve1.adm.auro.re - ```YAML - ansible_processor[0].find('Intel') != -1 - and ansible_architecture == 'x86_64' - ``` +[fleming:children] +fleming_pve +fleming_vm +``` + +> NB : +> +> L'exemple a été adapté de la configuration d'Aurore pour des raisons +> pédagogiques. Pour les fonctions (`proxy-server`, `dhcp-dynamique`…) il a été choisi de ne pas faire de groupe particulier mais plutôt de sélectionner/enlever @@ -74,10 +84,25 @@ qui peuvent ensuite être utilisés dans des variables. Pour lister tous les faits qu'Ansible collecte nativement d'un serveur on peut exécuter le module `setup` manuellement. -``` +```bash ansible proxy.adm.auro.re -m setup --ask-vault-pass ``` +Il est notamment possible de : + +* tester les versions de Debian, + + ```YAML + ansible_lsb.codename == 'stretch' + ``` + +* tester si c'est un CPU Intel x86_64, + + ```YAML + ansible_processor[0].find('Intel') != -1 + and ansible_architecture == 'x86_64' + ``` + ## Exécution d'Ansible ### Configurer la connexion au vlan adm @@ -88,7 +113,7 @@ Envoyer son agent SSH peut être dangereux On va utiliser plutôt `ProxyJump`. Dans la configuration SSH : -``` +```text Host *.adm.auro.re *.pve.auro.re # Accept new host keys StrictHostKeyChecking accept-new @@ -97,7 +122,8 @@ Host *.adm.auro.re *.pve.auro.re ProxyJump passerelle.auro.re ``` -Il faut sa clé SSH configurée sur le serveur que l'on déploit. +Il faut sa clé SSH configurée sur le serveur que l'on déploie. + ```bash ssh-copy-id proxy.adm.auro.re ``` @@ -107,6 +133,7 @@ ssh-copy-id proxy.adm.auro.re Il faut `python3-netaddr` sur sa machine. Pour tester le playbook `base.yml` : + ```bash ansible-playbook --ask-vault-pass base.yml --check ``` @@ -116,7 +143,7 @@ Vous pouvez ensuite enlever `--check` si vous voulez appliquer les changements ! Si vous avez des soucis de fingerprint ECDSA, vous pouvez ignorer une première fois (dangereux !) : `ANSIBLE_HOST_KEY_CHECKING=0 ansible-playbook...`. -### Ajouter tous les empruntes de serveur +### Ajouter toutes les empreintes de serveur ```bash #!/bin/bash @@ -140,8 +167,7 @@ ansible-galaxy collection install community.general ansible-galaxy collection install ansible.posix ``` - -Si vous n'arrivez pas à entrer votre _become password_ (bug dans ansible?), un +Si vous n'arrivez pas à entrer votre *become password* (bug dans ansible?), un workaround est le suivant : `$ export ANSIBLE_BECOME_PASS=''` @@ -149,20 +175,20 @@ workaround est le suivant : Notez l'espace au début pour ne pas log la commande dans votre historique shell. - ## Configuration des switchs depuis Ansible Afin d'acquérir de l'indépendance vis-à-vis de re2o, un module permettant de configurer les switchs depuis Ansible a été créé. Il utilise l'api rest des -switchs afin de récupérer et appliquer la configuration voulu. +switchs afin de récupérer et appliquer la configuration voulue. ### Prérequis Pour utiliser le module, il faut d'abord annoncer à Ansible qu'il ne faut pas effectuer de connexion ssh et de ne pas récupérer les faits. Cela se fait à -l'aide des variables `connection: httpapi` et `gather_facts: false`. Ensuite, -l'infrasutructue actuelle de Aurore nécéssite l'utilisation d'un proxy. Pour -cela, il suffit d'éxecuter la commande : +l'aide des variables `connection: httpapi` et `gather_facts: false` à placer +dans le playbook (pour une configuration locale) ou dans ansible.cfg (pour une +configuration globale). Ensuite, l'infrastructure actuelle de Aurore nécessite +l'utilisation d'un proxy. Pour cela, il suffit d'exécuter la commande : ```bash ssh -D 3000 switchs-manager.adm.auro.re @@ -220,7 +246,7 @@ tasks: Le `path` correspond à l'url de l'objet que l'on souhaite éditer et `data` correspond aux données qui seront envoyées dans une requête `PUT` (au format -`json`). Cependant, la configuration d'un vlan peut nécessité de le créer. +`json`). Cependant, la configuration d'un vlan peut nécessiter de le créer. Pour remédier à ce problème, il est possible d'utiliser la syntaxe suivante : ```yaml @@ -247,7 +273,7 @@ tasks: Le variable `create_method` correspond au type de la requête pour effectuer une action de création de l'objet. Il s'agit généralement de `POST`. Dans le cas où la variable n'est pas définit, la création sera désactivée et ainsi, si -l'url indiqué dans les `subpath` n'existe pas, alors la configuration échouera. +l'url indiquée dans les `subpath` n'existe pas, alors la configuration échouera. Par conséquent, si le vlan 42 a besoin d'être créé, une requête `POST` sera effectué sur l'url `vlans` avec les données dans `data`. @@ -274,7 +300,7 @@ l'éxecution, le module vérifiera si un `GET` sur l'url retourne une 404. > Remarque : > > Si les variables `delete` et `data` sont définies (dont `delete` à `true`), -> alors il en résultera une action de suppression malgrés tout. +> alors il en résultera une action de suppression malgré tout. Puisque `subpath` est une liste, il est possible de configurer plusieurs requête en même temps. Cela à l'avantage d'effectuer toutes les modifications à la suite From ef456e3ea2250a60249223781338b19cacd5f6fa Mon Sep 17 00:00:00 2001 From: korenstin Date: Thu, 28 Aug 2025 21:31:26 +0200 Subject: [PATCH 08/10] Remove switch-dev and update ntp --- group_vars/switch.yml | 2 +- host_vars/switch-dev.yml | 52 ---------------------------------------- hosts | 3 --- 3 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 host_vars/switch-dev.yml diff --git a/group_vars/switch.yml b/group_vars/switch.yml index 74d8bdf..aa0402c 100644 --- a/group_vars/switch.yml +++ b/group_vars/switch.yml @@ -7,6 +7,6 @@ glob_switch: operation_mode: SNTP_UNICAST_MODE poll_interval: 720 servers: - - ip: 10.130.0.15 + - ip: 10.206.1.5 priority: 1 ... diff --git a/host_vars/switch-dev.yml b/host_vars/switch-dev.yml deleted file mode 100644 index eda335f..0000000 --- a/host_vars/switch-dev.yml +++ /dev/null @@ -1,52 +0,0 @@ ---- -switch_vars: - name: switch-dev - location: Emilie du Chatelet - host: 10.130.4.199 - port: 80 - username: CHANGE - password: ME - vlans: - - id: 300 - name: "VLAN_TEST_300" - ipaddresses: - - mode: IAAM_DHCP - tagged: "{{ '10-12' | range2list }}" - - id: 301 - name: "VLAN_TEST_301" - ipaddresses: - - mode: IAAM_STATIC - ip: 10.203.4.199 - mask: 255.255.0.0 - - mode: IAAM_STATIC - ip: 10.204.4.199 - delete: true - tagged: [10, 11] - untagged: [12] - - id: 302 - name: "VLAN_TEST_302" - remove_ports: "{{ '10-12,13' | range2list }}" - delete_vlans: - - 400 - ports: - - id: 10 - name: "PORT_TEST_10" - enabled: false - loop_protect: true - lldp: true - - id: 11 - name: "PORT_TEST_11" - loop_protect: false - lldp: true - - id: 12 - name: "PORT_TEST_12" - loop_protect: true - - id: 13 - name: "PORT_TEST_13" - - id: 14 - name: "PORT_TEST_14" - loop_protect: true - - id: 15 - name: "PORT_TEST_15" - loop_protect: true -... diff --git a/hosts b/hosts index 37bbd6f..6dc745b 100644 --- a/hosts +++ b/hosts @@ -1,8 +1,5 @@ # Aurore servers inventory -[switch] -switch-dev - [vm_test] mx.test.infra.auro.re From 9a730f7dd9621bbc7ec80cc7fdf5aea3f88a7672 Mon Sep 17 00:00:00 2001 From: Vincent Lafeychine Date: Fri, 5 Sep 2025 11:25:23 +0200 Subject: [PATCH 09/10] dns: Remove grocy-bric + Add lafeychine.adh --- host_vars/ns-master.int.infra.auro.re/knotd.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/host_vars/ns-master.int.infra.auro.re/knotd.yml b/host_vars/ns-master.int.infra.auro.re/knotd.yml index 13864ef..5b86e79 100644 --- a/host_vars/ns-master.int.infra.auro.re/knotd.yml +++ b/host_vars/ns-master.int.infra.auro.re/knotd.yml @@ -152,9 +152,6 @@ knotd__hosts: portail-gs: - 10.53.0.247 - 2a09:6840:53::247 - grocy.bric: - - 45.66.111.133 - - 2a09:6840:111::133 adh.auro.re: paon: - 45.66.110.10 @@ -188,6 +185,8 @@ knotd__hosts: polaris: - 45.66.110.245 - 2a09:6840:110:0:dea6:32ff:feb4:d033 + lafeychine: + - 92.91.154.45 infra.auro.re: services-1.ceph: - 2a09:6840:214::1:1 From 1ec2cf8770d5196fb9ebc0fead128e2660aa1d44 Mon Sep 17 00:00:00 2001 From: Vincent Lafeychine Date: Mon, 8 Sep 2025 09:20:36 +0200 Subject: [PATCH 10/10] dns: Add tor.pub.infra.auro.re --- host_vars/ns-master.int.infra.auro.re/knotd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/host_vars/ns-master.int.infra.auro.re/knotd.yml b/host_vars/ns-master.int.infra.auro.re/knotd.yml index 5b86e79..6b0238b 100644 --- a/host_vars/ns-master.int.infra.auro.re/knotd.yml +++ b/host_vars/ns-master.int.infra.auro.re/knotd.yml @@ -349,6 +349,8 @@ knotd__hosts: - 45.66.111.207 ns-3.ovh: - 92.222.211.194 + tor.pub: + - 45.66.111.215 knotd__zones: auro.re: