Skip to content

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

  1. 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.
  2. 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ège SYS_ADMIN et aucune clé SSH. Plakar arbitrant la concurrence nativement via l'API, l'interface web reste accessible pendant les opérations d'élagage.
  3. 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.
  4. Découplage de l'élagage et de la compaction :
  5. 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.
  6. 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 %).
  7. Déport des fichiers temporaires : La variable TMPDIR et 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.