Configuration de Sauvegarde Plakar (API REST & Repli SSHFS)
Architecture de Sauvegarde Plakar (API REST & Repli SSHFS)
Cette documentation décrit une architecture de sauvegarde chiffrée et dédupliquée avec Plakar. Le système repose sur un mode hybride combinant un démon natif Plakar Server (API REST) pour les machines reliées au réseau local ou au VPN, une interface de visualisation Web conteneurisée, un mécanisme de repli en SSHFS via bastion pour les machines nomades, et une maintenance automatisée via des timers Systemd.
1. Contexte & Architecture Technique
Problématique du mode FUSE/SSHFS exclusif
Dans un déploiement basé exclusivement sur des montages distants via SSHFS :
- Latence réseau sur les métadonnées : Les opérations de parcours de dépôt (recherche de blocs, calculs d'index) effectuent des milliers d'appels SFTP synchrones. Sur un disque mécanique distant accédé via un tunnel réseau, le débit effectif chute fortement en raison de la latence réseau par aller-retour (RTT).
- Verrous exclusifs et blocage des services : L'opération de réorganisation physique des données (plakar maintenance) pose un verrou exclusif sur le dépôt. L'interface web et les sauvegardes des clients restent bloquées tant que cette opération n'est pas achevée.
- Saturation de la mémoire vive : Par défaut, l'écriture de paquets temporaires dans /tmp (souvent monté en tmpfs en mémoire vive) peut saturer la RAM sur des machines hébergeant des conteneurs.
Principes de l'architecture retenue
- Démon Plakar Server (API REST) : Exécuté sous Systemd sur le serveur de stockage (
10.0.0.8:9876), il écoute sur le réseau privé sécurisé. Il traite les requêtes d'écriture et de lecture directement sur le disque local, éliminant la surcouche FUSE/SSHFS pour les hôtes connectés au réseau interne. - Interface Web conteneurisée (
plakar-web) : Déployée en conteneur Docker, elle interroge l'API REST de Plakar Server via HTTP. Elle ne nécessite aucun montage FUSE, aucun privilègeSYS_ADMINet aucune clé SSH. Plakar arbitrant la concurrence nativement via l'API, l'interface web reste accessible pendant les opérations d'élagage. - Agent dual-mode (REST API et repli SSHFS) : L'agent vérifie la joignabilité de l'API REST. Si elle répond, la sauvegarde s'effectue directement via HTTP. Dans le cas contraire (ex. machine nomade hors du réseau interne), le script bascule automatiquement sur un montage SSHFS relayé par un bastion SSH.
- Découplage de l'élagage et de la compaction :
- L'élagage logique (
plakar prune), qui supprime les métadonnées des instantanés expirés (> 120 jours), s'exécute de façon hebdomadaire en quelques secondes. - La compaction physique des paquets (
plakar maintenance) est dissociée de la routine hebdomadaire et n'est exécutée que si l'espace disque libre du serveur de stockage passe sous un seuil configurable (ex: 25 %). - Déport des fichiers temporaires : La variable
TMPDIRet le répertoire des packfiles temporaires sont explicitement configurés sur un stockage persistant (/var/tmp) pour préserver la mémoire vive.
2. Diagramme d'Architecture Globale
Le schéma ci-dessous illustre l'organisation des flux entre les clients de sauvegarde, l'interface web, le serveur de stockage et le bastion de rebond :
flowchart TD
subgraph Users["Accès Utilisateurs & Administration"]
Browser["Navigateur Web / Tableau de bord<br/>http://10.0.1.39:8080"]
AdminCLI["Client CLI d'administration<br/>plakar at http://10.0.0.8:9876 ..."]
end
subgraph CentralHost["Serveur Applicatif / Administration (10.0.1.39)"]
DockerWeb["Conteneur Docker : plakar-web<br/>UI Web HTTP (port 8080)<br/>Connexion directe REST API"]
MaintService["Service Systemd : plakar-maintenance<br/>Script : ansible-script-prune.sh<br/>Timer : Jeudi 04:00"]
LocalCache["Cache local<br/>/root/.cache/plakar"]
DockerWeb --> LocalCache
MaintService --> LocalCache
end
subgraph Network["Réseau"]
VPN["Réseau privé / VPN (10.0.0.0/24)<br/>IP Serveur de stockage : 10.0.0.8"]
Bastion["Bastion SSH (bastion.example.com)<br/>Accès de secours pour machines distantes"]
end
subgraph StorageServer["Serveur de Stockage (backup.example.com - 10.0.0.8)"]
PlakarDaemon["Démon Plakar Server (REST API)<br/>plakar-server.service : port 9876<br/>Option : -allow-delete"]
Storage["Stockage local (/mnt/backup/plakar)<br/>• Packfiles dédupliqués chiffrés<br/>• backup-hosts.txt (registre hôtes)<br/>• clients-backups.txt (historique dates)"]
PlakarDaemon --> Storage
end
subgraph Clients["Clients de Sauvegarde"]
direction TB
IntServer["Serveurs internes (réseau privé)<br/>ansible-script-backup.sh (Mercredi 03:00)"]
NomadClient["Machines nomades (hors réseau privé)<br/>ansible-script-backup.sh (via Bastion)"]
end
Browser -->|"HTTP Web UI (8080)"| DockerWeb
AdminCLI -->|"Requêtes REST API (port 9876)"| VPN
DockerWeb -->|"Connexion REST API<br/>PLAKAR_SERVER_URL=http://10.0.0.8:9876"| VPN
MaintService -->|"Élagage REST API<br/>plakar at http://10.0.0.8:9876 prune"| VPN
IntServer -->|"1. Mode direct : API REST<br/>http://10.0.0.8:9876"| VPN
NomadClient -->|"2. Mode repli : SSHFS via Bastion<br/>ProxyCommand port 22"| Bastion
VPN -->|"Port 9876"| PlakarDaemon
Bastion -->|"SSH / SFTP (port 22)"| Storage
3. Paramètres de Configuration
Avant de déployer les playbooks, ajustez les variables cibles selon votre environnement :
| Variable | Description | Exemple générique |
|---|---|---|
plakar_passphrase |
Clé de déchiffrement du dépôt Plakar | YOUR_SECRET_PASSPHRASE_HERE |
plakar_server_ip |
Adresse IP d'écoute de Plakar Server sur le réseau privé | 10.0.0.8 |
plakar_server_port |
Port TCP de service de Plakar Server | 9876 |
plakar_server_url |
URL complète de l'API REST de Plakar Server | http://10.0.0.8:9876 |
remote_backup_user |
Compte utilisateur dédié sur le serveur de stockage | backup_user |
remote_backup_host |
Nom d'hôte ou IP du serveur de sauvegarde | backup.example.com |
remote_bastion_host |
Nom d'hôte ou IP du bastion SSH de rebond | bastion.example.com |
remote_backup_path |
Chemin du dépôt sur le serveur de stockage | /mnt/backup/plakar |
min_free_percent |
Seuil d'espace libre minimal déclenchant la compaction | 25 (%) |
Note
Cloisonnement réseau : Le port 9876 de Plakar Server écoute uniquement sur l'adresse du réseau privé (10.0.0.8). Il ne doit pas être exposé sur les interfaces publiques.
4. Déploiement de l'Agent de Sauvegarde Dual-Mode
Le playbook Ansible installe Plakar sur les serveurs à sauvegarder, configure le script maître et active le timer Systemd. Le serveur de stockage est exclu de ce déploiement via la clause hosts: "servers_to_backup:!storage_server".
Logique d'exécution de l'agent
flowchart TD
Start["Déclenchement timer (Mercredi 03:00)"] --> LockCheck{"Verrou flock actif ?<br/>(/var/lock/ansible-script-backup.lock)"}
LockCheck -->|"Oui"| ExitBusy["Arrêt : Sauvegarde déjà en cours"]
LockCheck -->|"Non"| CheckDate{"Dernière sauvegarde < 7 jours ?<br/>(Lecture clients-backups.txt)"}
CheckDate -->|"Oui"| ExitRecent["Arrêt : Sauvegarde récente déjà effectuée"]
CheckDate -->|"Non"| PingAPI{"Test joignabilité Plakar Server<br/>curl -s -f http://10.0.0.8:9876"}
PingAPI -->|"Succès (200 OK)"| ModeHTTP["Mode HTTP REST API direct<br/>PLAKAR_REPOSITORY=http://10.0.0.8:9876"]
PingAPI -->|"Échec / Timeout"| ModeSSHFS["Mode repli SSHFS via Bastion<br/>PLAKAR_REPOSITORY=/mnt/remote-backup"]
ModeHTTP --> DumpMeta["Export des métadonnées système<br/>(Paquets APT, Flatpak, sources.list)"]
ModeSSHFS --> DumpMeta
DumpMeta --> BuildExcl["Génération des exclusions<br/>CACHEDIR.TAG, répertoires de cache"]
BuildExcl --> RunBackup["Exécution sauvegarde Plakar<br/>TMPDIR=/var/tmp (Stockage persistant)<br/>plakar backup -tag $HOSTNAME /"]
RunBackup --> IntegrityCheck{"Contrôle d'intégrité rapide<br/>plakar check -fast -latest -tag $HOSTNAME"}
IntegrityCheck -->|"Succès"| LogSuccess["Mise à jour clients-backups.txt"]
IntegrityCheck -->|"Échec"| AlertError["Signalement d'erreur d'intégrité"]
LogSuccess --> Cleanup["Trap cleanup EXIT :<br/>Purge /var/tmp, démontage SSHFS si actif"]
AlertError --> Cleanup
Playbook Ansible : Déploiement Agent (playbook-backup_agent.yaml)
---
- name: Installer et configurer l'agent de sauvegarde Plakar
hosts: "servers_to_backup:!storage_server"
gather_facts: false
become: true
become_method: sudo
vars:
plakar_passphrase: "YOUR_SECRET_PASSPHRASE_HERE"
plakar_server_url: "http://10.0.0.8:9876"
remote_backup_user: "backup_user"
remote_backup_host: "backup.example.com"
remote_bastion_host: "bastion.example.com"
remote_backup_path: "/mnt/backup/plakar"
ssh_key_path: "/root/.ssh/id_rsa"
tasks:
- name: Installation et configuration de Plakar
block:
- name: Installer les dépendances requises
ansible.builtin.package:
name:
- curl
- gnupg2
- sshfs
- python3-debian
state: present
- name: Vérifier la clé GPG Plakar
ansible.builtin.command: gpg --show-keys /usr/share/keyrings/plakar.gpg
register: plakar_key_check
check_mode: false
changed_when: false
failed_when: false
- name: Supprimer l'ancienne clé GPG si obsolète
ansible.builtin.file:
path: /usr/share/keyrings/plakar.gpg
state: absent
when:
- not (plakar_key_check.skipped | default(false))
- (plakar_key_check.rc | default(-1)) == 0
- "'29F8A046090829187271004F86A1BA757543A86A' not in (plakar_key_check.stdout | default(''))"
- name: Télécharger et importer la clé GPG officielle
ansible.builtin.shell: |
set -o pipefail
curl -fsSL https://plakar.io/dist/keys/plakar-packages.gpg | gpg --dearmor --yes -o /usr/share/keyrings/plakar.gpg
args:
creates: /usr/share/keyrings/plakar.gpg
executable: /bin/bash
- name: Configurer le dépôt officiel Plakar (Deb822)
ansible.builtin.deb822_repository:
name: plakar
types: [deb]
uris: [https://plakar.io/dist/repos/deb/]
suites: [stable]
components: [main]
signed_by: /usr/share/keyrings/plakar.gpg
state: present
- name: Installer le paquet Plakar
ansible.builtin.package:
name: plakar
state: present
- name: Vérifier la version installée
ansible.builtin.command: plakar version
register: plakar_version
changed_when: false
- name: Créer le répertoire /opt/ansible
ansible.builtin.file:
path: /opt/ansible
state: directory
mode: '0755'
owner: root
group: root
- name: Déployer le script de sauvegarde maître
ansible.builtin.copy:
src: ansible-script-backup.sh
dest: /opt/ansible/ansible-script-backup.sh
mode: '0700'
owner: root
group: root
- name: Créer le service Systemd de sauvegarde
ansible.builtin.copy:
dest: /etc/systemd/system/plakar-backup.service
mode: '0644'
content: |
[Unit]
Description=Plakar Backup Service
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/ansible/ansible-script-backup.sh
User=root
TimeoutStartSec=48h
- name: Créer le timer Systemd hebdomadaire
ansible.builtin.copy:
dest: /etc/systemd/system/plakar-backup.timer
mode: '0644'
content: |
[Unit]
Description=Run Plakar Backup every Wednesday at 3AM
[Timer]
OnCalendar=Wed *-*-* 03:00:00
RandomizedDelaySec=6h
Persistent=true
[Install]
WantedBy=timers.target
- name: Activer et démarrer le timer Systemd
ansible.builtin.systemd:
name: plakar-backup.timer
state: started
enabled: yes
daemon_reload: yes
Script de Sauvegarde Maître (/opt/ansible/ansible-script-backup.sh)
#!/bin/bash
set -eo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "Ce script doit être exécuté avec les privilèges root." >&2
exit 1
fi
# Verrou d'exécution unique local
exec 200>/var/lock/ansible-script-backup.lock
if ! flock -n 200; then
echo "Une sauvegarde Plakar est déjà en cours d'exécution. Arrêt."
exit 0
fi
plakar -disable-security-check >/dev/null 2>&1 || true
# Configuration générale
REMOTE_USER="backup_user"
REMOTE_HOST="backup.example.com"
BASTION_HOST="bastion.example.com"
REMOTE_PATH="/mnt/backup/plakar"
SSH_KEY_PATH="/root/.ssh/id_rsa"
LOCAL_MOUNT_POINT="/mnt/remote-backup"
export PLAKAR_PASSPHRASE="YOUR_SECRET_PASSPHRASE_HERE"
# Connexion API REST
PLAKAR_SERVER_IP="10.0.0.8"
PLAKAR_SERVER_PORT="9876"
PLAKAR_SERVER_URL="http://${PLAKAR_SERVER_IP}:${PLAKAR_SERVER_PORT}"
CACHE_DIR="/root/.cache/plakar"
mkdir -p "$CACHE_DIR"
# Déport TMPDIR sur stockage persistant (évite saturation du tmpfs en RAM)
export TMPDIR="/var/tmp"
PACKFILES_DIR="/var/tmp/plakar-packfiles"
mkdir -p "$PACKFILES_DIR"
HOSTNAME_TAG=$(hostname)
EXCLUDES_FILE="/var/tmp/plakar-excludes-${HOSTNAME_TAG}.txt"
cleanup() {
rm -f "$EXCLUDES_FILE" 2>/dev/null || true
rm -rf "$PACKFILES_DIR" 2>/dev/null || true
if mountpoint -q "$LOCAL_MOUNT_POINT" 2>/dev/null; then
fusermount -uz "$LOCAL_MOUNT_POINT" 2>/dev/null || true
fi
}
trap cleanup EXIT
# Détection du mode de connexion
USE_HTTP_MODE=false
echo "--- Détection du mode de connexion au dépôt Plakar ---"
if curl -s -f -m 5 "$PLAKAR_SERVER_URL" >/dev/null 2>&1; then
echo "Plakar Server joignable sur $PLAKAR_SERVER_URL (Mode HTTP direct)."
USE_HTTP_MODE=true
PLAKAR_REPOSITORY="$PLAKAR_SERVER_URL"
else
echo "Plakar Server non joignable sur $PLAKAR_SERVER_URL, bascule en mode SSHFS via Bastion."
USE_HTTP_MODE=false
PLAKAR_REPOSITORY="$LOCAL_MOUNT_POINT"
fi
if [ "$USE_HTTP_MODE" = false ]; then
mkdir -p "$HOME/.ssh"
ssh-keyscan -H "$REMOTE_HOST" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
ssh-keyscan -H "$BASTION_HOST" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
mkdir -p "$LOCAL_MOUNT_POINT"
if ! mountpoint -q "$LOCAL_MOUNT_POINT"; then
sshfs "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}" "$LOCAL_MOUNT_POINT" \
-o "IdentityFile=${SSH_KEY_PATH}",reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,allow_other,StrictHostKeyChecking=no,ProxyCommand="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ${SSH_KEY_PATH} -W %h:%p ${REMOTE_USER}@${BASTION_HOST}"
fi
if [ ! -f "$PLAKAR_REPOSITORY/CONFIG" ]; then
echo "Dépôt non initialisé. Création..."
plakar -cachedir "$CACHE_DIR" at "$PLAKAR_REPOSITORY" create
fi
fi
# Contrôle d'intervalle (7 jours)
check_backup_needed() {
local target="$1"
local last_date_str=""
if [ "$USE_HTTP_MODE" = false ] && [ -f "$LOCAL_MOUNT_POINT/clients-backups.txt" ]; then
last_date_str=$(grep "^${HOSTNAME_TAG}:" "$LOCAL_MOUNT_POINT/clients-backups.txt" | cut -d':' -f2- | xargs || true)
fi
if [ -z "$last_date_str" ]; then
last_date_str=$(plakar -cachedir "$CACHE_DIR" at "$PLAKAR_REPOSITORY" ls -tag "$HOSTNAME_TAG" 2>/dev/null | awk -v tgt="$target" '$NF == tgt {print $1; exit}' || true)
fi
if [ -n "$last_date_str" ]; then
local last_ts
last_ts=$(date -d "$last_date_str" +%s 2>/dev/null || echo 0)
local current_ts
current_ts=$(date +%s)
local elapsed_seconds=$((current_ts - last_ts))
if [ "$elapsed_seconds" -lt 604800 ]; then
return 1 # Moins de 7 jours écoulés
fi
fi
return 0 # Sauvegarde nécessaire
}
if ! check_backup_needed "/"; then
echo "Dernière sauvegarde récente (< 7 jours). Tâche ignorée."
exit 0
fi
# Export des métadonnées de paquets
mkdir -p /opt/backup-meta
command -v apt-mark &>/dev/null && apt-mark showmanual > "/opt/backup-meta/apt-manual.txt" 2>/dev/null || true
command -v flatpak &>/dev/null && flatpak list --columns=application --app > "/opt/backup-meta/flatpak-list.txt" 2>/dev/null || true
[ -d "/etc/apt/sources.list.d" ] && cat /etc/apt/sources.list.d/* > "/opt/backup-meta/sources-list.txt" 2>/dev/null || true
# Fichier d'exclusions
cat << 'EOF' > "$EXCLUDES_FILE"
*.mp4
*.mkv
*.avi
*.mov
*.wmv
/proc/*
/sys/*
/dev/*
/run/*
/tmp/*
/swapfile
/mnt/remote-backup/*
/mnt/backup/*
/var/tmp/plakar-*
**/.cache/**
**/[cC]ache/**
**/GPUCache/**
**/__pycache__/**
**/.local/share/Trash/**
**/.local/share/containers/**
EOF
# Prise en compte des répertoires balisés CACHEDIR.TAG
find /opt /home /mnt -type f -name "CACHEDIR.TAG" 2>/dev/null | while read -r tag_file; do
echo "$(dirname "$tag_file")/**" >> "$EXCLUDES_FILE"
done
# Exécution de la sauvegarde
echo "Lancement de la sauvegarde Plakar..."
plakar -cachedir "$CACHE_DIR" at "$PLAKAR_REPOSITORY" backup \
-no-progress \
-tag "$HOSTNAME_TAG" \
-packfiles "$PACKFILES_DIR" \
-ignore-file "$EXCLUDES_FILE" \
"/"
# Validation d'intégrité
echo "Contrôle d'intégrité du cliché..."
if plakar -cachedir "$CACHE_DIR" at "$PLAKAR_REPOSITORY" check -fast -latest -tag "$HOSTNAME_TAG"; then
echo "Intégrité validée."
if [ "$USE_HTTP_MODE" = false ] && [ -d "$LOCAL_MOUNT_POINT" ]; then
sed -i "/^${HOSTNAME_TAG}:/d" "$LOCAL_MOUNT_POINT/clients-backups.txt" 2>/dev/null || true
echo "${HOSTNAME_TAG}: $(date +'%Y-%m-%d %H:%M:%S')" >> "$LOCAL_MOUNT_POINT/clients-backups.txt"
fi
else
echo "Échec du contrôle d'intégrité de la sauvegarde !" >&2
exit 1
fi
exit 0
5. Serveur Plakar Centralisé & Interface Web (Web UI)
1. Configuration de Plakar Server (backup.example.com)
Sur le serveur de stockage, le service Systemd plakar-server.service exécute le démon Plakar en écoute sur 10.0.0.8:9876. L'option -allow-delete permet de traiter les demandes d'élagage provenant des clients autorisés sur le réseau privé.
[Unit]
Description=Plakar Storage Server Daemon (REST API)
After=network.target local-fs.target
[Service]
Type=simple
User=backup_user
Group=backup_user
Environment=PLAKAR_PASSPHRASE=YOUR_SECRET_PASSPHRASE_HERE
ExecStart=/usr/bin/plakar at /mnt/backup/plakar server -allow-delete -listen 10.0.0.8:9876
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
2. Conteneur Docker Web UI (plakar-web)
L'interface web tourne dans un conteneur Docker sur le serveur applicatif ou d'administration. Elle interroge l'URL HTTP de Plakar Server.
Extrait Docker Compose (docker-compose.yaml)
plakar-web:
build:
context: ./plakar_docker
container_name: plakar-web
restart: always
environment:
- PLAKAR_PASSPHRASE=${PLAKAR_PASSPHRASE}
- PLAKAR_SERVER_URL=http://10.0.0.8:9876
volumes:
- /opt/plakar/cache:/root/.cache/plakar
healthcheck:
test: ["CMD-SHELL", "curl --fail http://localhost:8080/ || exit 1"]
interval: 60s
timeout: 30s
retries: 5
start_period: 30s
networks:
internal-network:
ipv4_address: 10.0.1.39
Script d'entrée du conteneur (entrypoint.sh)
#!/bin/bash
set -e
PLAKAR_SERVER_URL="${PLAKAR_SERVER_URL:-http://10.0.0.8:9876}"
plakar -disable-security-check >/dev/null 2>&1 || true
if [ -n "$PLAKAR_SERVER_URL" ]; then
echo "Démarrage de Plakar UI via serveur REST : $PLAKAR_SERVER_URL..."
exec plakar at "$PLAKAR_SERVER_URL" ui -addr 0.0.0.0:8080 -no-spawn
fi
echo "Démarrage de Plakar UI via point de montage local..."
exec plakar at /mnt/plakar ui -addr 0.0.0.0:8080 -no-spawn
6. Maintenance Centralisée et Élagage Intelligent
La maintenance s'exécute de façon planifiée chaque jeudi à 04h00 sur le serveur d'administration.
Workflow d'élagage et de compaction
flowchart TD
StartMaint["Timer systemd (Jeudi 04:00)"] --> TestConn{"Vérification API REST<br/>curl http://10.0.0.8:9876"}
TestConn -->|"Erreur"| MaintFail["Arrêt : Plakar Server non joignable"]
TestConn -->|"Succès"| FetchHosts["Lecture de backup-hosts.txt<br/>sur le serveur de stockage"]
FetchHosts --> LoopPrune["Élagage : plakar prune -days 120<br/>Pour chaque tag d'hôte"]
LoopPrune --> PruneDone["Suppression des métadonnées expirées"]
PruneDone --> CheckDisk{"Espace disque libre (df)<br/>Espace libre <= 25% ou option --force ?"}
CheckDisk -->|"Non (> 25% libre)"| SkipCompaction["Compaction physique ignorée<br/>(Pas de verrouillage inutile)"]
CheckDisk -->|"Oui (Seuil atteint ou forcé)"| RunCompaction["Exécution de plakar maintenance<br/>Compaction physique des packfiles"]
SkipCompaction --> MaintSuccess["Fin de maintenance avec succès"]
RunCompaction --> MaintSuccess
Playbook Ansible : Déploiement de la Maintenance (playbook-backup_maintenance.yaml)
---
- name: Configuration de Plakar Server sur le serveur de stockage
hosts: storage_server
gather_facts: false
become: true
become_method: sudo
vars:
plakar_listen_ip: "10.0.0.8"
plakar_listen_port: "9876"
plakar_passphrase: "YOUR_SECRET_PASSPHRASE_HERE"
tasks:
- name: Déploiement du service systemd plakar-server
ansible.builtin.copy:
dest: /etc/systemd/system/plakar-server.service
mode: '0644'
content: |
[Unit]
Description=Plakar Storage Server Daemon (REST API)
After=network.target local-fs.target
[Service]
Type=simple
User=backup_user
Group=backup_user
Environment=PLAKAR_PASSPHRASE={{ plakar_passphrase }}
ExecStart=/usr/bin/plakar at /mnt/backup/plakar server -allow-delete -listen {{ plakar_listen_ip }}:{{ plakar_listen_port }}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
- name: Activer et démarrer plakar-server.service
ansible.builtin.systemd:
name: plakar-server.service
state: restarted
enabled: yes
daemon_reload: yes
- name: Vérifier l'écoute sur le port du serveur Plakar
ansible.builtin.wait_for:
host: "{{ plakar_listen_ip }}"
port: "{{ plakar_listen_port }}"
timeout: 15
- name: Déploiement et configuration de la maintenance sur le serveur d'administration
hosts: central_management_server
gather_facts: false
become: true
become_method: sudo
vars:
prune_script_dest: "/opt/ansible/ansible-script-prune.sh"
plakar_server_url: "http://10.0.0.8:9876"
tasks:
- name: Répertoires /opt/ansible et cache local
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
mode: "{{ item.mode }}"
owner: root
group: root
loop:
- { path: "/opt/ansible", mode: "0755" }
- { path: "/root/.cache/plakar", mode: "0700" }
- name: Déployer le script d'élagage REST API
ansible.builtin.copy:
src: ansible-script-prune.sh
dest: "{{ prune_script_dest }}"
mode: '0700'
owner: root
group: root
- name: Créer le service Systemd de maintenance
ansible.builtin.copy:
dest: /etc/systemd/system/plakar-maintenance.service
mode: '0644'
content: |
[Unit]
Description=Plakar Repository Pruning and Maintenance Service
After=network.target
[Service]
Type=oneshot
ExecStart={{ prune_script_dest }}
User=root
TimeoutStartSec=72h
- name: Créer le timer Systemd hebdomadaire (Jeudi 04:00)
ansible.builtin.copy:
dest: /etc/systemd/system/plakar-maintenance.timer
mode: '0644'
content: |
[Unit]
Description=Run Plakar Repository Pruning and Maintenance every Thursday at 4AM
[Timer]
OnCalendar=Thu *-*-* 04:00:00
Persistent=true
[Install]
WantedBy=timers.target
- name: Activer et démarrer le timer Systemd
ansible.builtin.systemd:
name: plakar-maintenance.timer
state: started
enabled: yes
daemon_reload: yes
- name: Valider la connectivité REST API vers Plakar Server
ansible.builtin.uri:
url: "{{ plakar_server_url }}"
method: GET
status_code: 200
timeout: 10
- name: Vérifier l'ancienneté des sauvegardes des clients (alerte 30 jours)
ansible.builtin.shell: |
python3 -c '
import datetime, sys
filename = "/mnt/backup/plakar/clients-backups.txt"
now = datetime.datetime.now()
limit = datetime.timedelta(days=30)
errors = []
try:
with open(filename, "r") as f:
lines = f.readlines()
except FileNotFoundError:
sys.exit(0)
for line in lines:
if not line.strip() or line.startswith("#"): continue
try:
parts = line.strip().split(": ")
if len(parts) != 2: continue
client, date_str = parts[0], parts[1].strip()
backup_date = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
if now - backup_date > limit:
errors.append(f"Le client {client} n-a pas ete sauvegarde depuis le {date_str} (> 30 jours)")
except Exception as e:
sys.exit(1)
if errors:
for error in errors: print(error)
sys.exit(1)
print("Toutes les sauvegardes des clients sont récentes.")
'
delegate_to: storage_server
become: false
changed_when: false
Script d'Élagage (/opt/ansible/ansible-script-prune.sh)
#!/bin/bash
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "Ce script doit être exécuté avec les privilèges root." >&2
exit 1
fi
plakar -disable-security-check >/dev/null 2>&1 || true
# Configuration
PLAKAR_SERVER_URL="http://10.0.0.8:9876"
CACHE_DIR="/root/.cache/plakar"
PRUNE_DAYS=120
SSH_KEY_PATH="/root/.ssh/id_rsa"
REMOTE_USER="backup_user"
REMOTE_HOST="backup.example.com"
REMOTE_MOUNT="/mnt/backup"
export PLAKAR_PASSPHRASE="YOUR_SECRET_PASSPHRASE_HERE"
export TMPDIR="/var/tmp"
FORCE_COMPACTION=false
for arg in "$@"; do
case "$arg" in
-f|--force|--force-compaction) FORCE_COMPACTION=true ;;
esac
done
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; }
log "=== Début de l'élagage Plakar ==="
# 1. Vérification de connectivité
if ! curl -s -f -m 5 "$PLAKAR_SERVER_URL" >/dev/null 2>&1; then
log "Impossible de joindre Plakar Server sur $PLAKAR_SERVER_URL. Arrêt." >&2
exit 1
fi
# 2. Récupération des hôtes enregistrés
HOSTS_LIST=()
if [ -f "$SSH_KEY_PATH" ]; then
mapfile -t HOSTS_LIST < <(ssh -i "$SSH_KEY_PATH" -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat ${REMOTE_MOUNT}/plakar/backup-hosts.txt" 2>/dev/null || true)
fi
# 3. Élagage par tag
VALID_HOSTS=()
for host in "${HOSTS_LIST[@]}"; do
[[ -z "$host" || "$host" =~ ^# ]] && continue
host=$(echo "$host" | xargs)
[ -n "$host" ] && VALID_HOSTS+=("$host")
done
if [ "${#VALID_HOSTS[@]}" -gt 0 ]; then
for tag in "${VALID_HOSTS[@]}"; do
log "Élagage pour le tag : $tag"
plakar -cachedir "$CACHE_DIR" at "$PLAKAR_SERVER_URL" prune -days "$PRUNE_DAYS" -tag "$tag" -apply || true
done
else
log "Élagage global par défaut ($PRUNE_DAYS jours)..."
plakar -cachedir "$CACHE_DIR" at "$PLAKAR_SERVER_URL" prune -days "$PRUNE_DAYS" -apply || true
fi
# 4. Contrôle d'espace disque et compaction conditionnelle
MIN_FREE_PERCENT=25
TRIGGER_COMPACTION=false
DISK_USED_PCT=""
if [ -f "$SSH_KEY_PATH" ]; then
DISK_USED_PCT=$(ssh -i "$SSH_KEY_PATH" -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "df ${REMOTE_MOUNT} --output=pcent | tail -n 1" 2>/dev/null | tr -dc '0-9' || true)
fi
if [ -n "$DISK_USED_PCT" ]; then
DISK_FREE_PCT=$(( 100 - DISK_USED_PCT ))
log "Espace disque distant : ${DISK_USED_PCT}% utilisé (${DISK_FREE_PCT}% libre)."
if [ "$DISK_FREE_PCT" -le "$MIN_FREE_PERCENT" ]; then
log "Seuil d'espace libre atteint (${DISK_FREE_PCT}% <= ${MIN_FREE_PERCENT}%). Déclenchement de la compaction."
TRIGGER_COMPACTION=true
fi
fi
if [ "$FORCE_COMPACTION" = true ]; then
log "Compaction physique forcée via option."
TRIGGER_COMPACTION=true
fi
if [ "$TRIGGER_COMPACTION" = true ]; then
log "Exécution de plakar maintenance..."
plakar -cachedir "$CACHE_DIR" at "$PLAKAR_SERVER_URL" maintenance
else
log "Espace disque suffisant. Compaction physique non requise."
fi
log "=== Fin de la tâche de maintenance ==="
7. Commandes d'Exploitation Usuelles
Toutes les opérations d'inspection et de restauration s'exécutent directement en ciblant l'URL de Plakar Server, sans montage FUSE local :
export PLAKAR_PASSPHRASE="YOUR_SECRET_PASSPHRASE_HERE"
REPO="http://10.0.0.8:9876"
# Définition d'un alias local (optionnel)
plakar config remote set central location http://10.0.0.8:9876
# Lister les sauvegardes du dépôt
plakar at $REPO ls
# Filtrer par machine
plakar at $REPO ls -tag "server-web"
# Détails d'un cliché
plakar at $REPO info <snapshot-id>
# Explorer les fichiers d'un cliché
plakar at $REPO ls <snapshot-id>:/etc/
# Rechercher un fichier
plakar at $REPO find -tag "server-web" "*.conf"
# Restaurer une arborescence
plakar at $REPO pull <snapshot-id>:/var/www/ -to /tmp/restauration/
# Contrôle d'intégrité rapide
plakar at $REPO check -fast -latest -tag "server-web"
# Déclencher manuellement la compaction physique
/opt/ansible/ansible-script-prune.sh --force
8. Retours d'Expérience et Points de Vigilance
| Problème rencontré | Cause technique | Solution mise en œuvre |
|---|---|---|
| Maintenance excessivement longue à distance | L'accès FUSE/SSHFS à travers un réseau distant générait des millions d'appels SFTP synchrones sur un disque mécanique. | Découplage : élagage des métadonnées (prune) hebdomadaire, et compaction (maintenance) déclenchée uniquement sous un seuil d'espace libre (< 25 %). |
| Indisponibilité de l'interface web | Le verrou exclusif de la compaction bloquait les lectures de plakar-web. |
Migration de l'interface web vers l'API REST de Plakar Server, qui arbitre les accès concurrents. |
Saturation mémoire sous /tmp |
Plakar écrivait ses paquets temporaires dans /tmp monté en tmpfs RAM. |
Déport forcé de TMPDIR et PACKFILES_DIR vers /var/tmp (stockage persistant). |
| Coupures de lien réseau sous charge soutenue | Bug de pilote Ethernet (ex: contrôleurs Intel e1000e) lié au TSO/GSO. |
Désactivation des options d'accélération matérielle tso, gso et eee sur l'interface concernée via ethtool et NetworkManager. |
| Instabilité de tunnel VPN | Scripts de surveillance réseau trop agressifs réinitialisant l'interface au moindre paquet perdu. | Lissage de la vérification : contrôle de la récence du dernier échange de clés (latest-handshakes < 180s) avant relance de l'interface. |