CDC_backupmanager_ynh.md 20 KB

Cahier des charges — backupmanager_ynh

Version 5.0 — 2026-07-17

1. Vision générale

Application YunoHost centralisant toutes les sauvegardes d'un serveur, s'appuyant au maximum sur les outils YunoHost natifs, extensible en écosystème fédéré multi-instances.

Périmètre : Apps YNH · Système YNH · Répertoires custom · MySQL · PostgreSQL

┌──────────────┐  API  ┌──────────────┐  API  ┌──────────────┐
│  Instance A  │◄─────►│  Instance B  │◄─────►│  Instance C  │
│  (maître)    │       │  (nœud)      │       │  (nœud)      │
└──────────────┘       └──────────────┘       └──────────────┘
  N'importe quelle instance peut être "maître"

2. Décisions clés

Sujet Décision
Langage Python 3 + Flask
Scheduler APScheduler + SQLite
DB SQLite + SQLAlchemy
Frontend Jinja2 + TailwindCSS CDN
Proxy/Service Nginx + systemd YunoHost natifs
Archives /home/yunohost.backup/archives/
Nommage jerry_nocodb_20260508.tar
Auth dashboard SSO YunoHost — admins uniquement
Auth API /api ouvert SSOwat + token X-BackupManager-Key
Transfert HTTP chunked (fédération) + SSH/rsync (destinations SSH)
Restauration Complète : fichiers + user système + systemd + DB
Destinations Many-to-many : un job peut cibler plusieurs destinations

3. Structure du dépôt

backupmanager_ynh/
├── manifest.toml
├── scripts/
│   ├── _common.sh
│   ├── install / remove / backup / restore / upgrade
├── conf/
│   ├── nginx.conf
│   ├── systemd.service
│   ├── app.conf.j2
│   └── sudoers
├── sources/
│   ├── app.py                  # Bootstrap Flask + enregistrement blueprints
│   ├── scheduler.py            # APScheduler
│   ├── db.py                   # SQLAlchemy (Job, Run, Destination, Setting, …)
│   ├── retention.py            # Moteur count/daily/gfs + apply_ssh_retention
│   ├── notifications.py        # Email SMTP (succès/erreur)
│   ├── helpers.py              # read_archive_info, get_ynh_apps
│   ├── blueprints/
│   │   ├── jobs.py             # Dashboard local, CRUD jobs, archives, restauration
│   │   ├── destinations.py     # CRUD destinations SSH
│   │   ├── network.py          # Fédération, push/pull inter-instances
│   │   ├── settings.py         # Paramètres SMTP, export/import config JSON
│   │   ├── api.py              # API REST v1 (auth par token)
│   │   └── overview.py         # Vue globale des archives (local + SSH + instances)
│   ├── jobs/
│   │   ├── ynh_backup.py       # Point d'entrée execute_job, ynh_app, ynh_system
│   │   ├── custom_dir.py       # tar + rsync chemins libres
│   │   ├── db_dump.py          # mysqldump / pg_dump
│   │   ├── transfer.py         # rsync SSH + push HTTP chunked
│   │   └── utils.py            # sudo_exists/getsize/listdir/rm/rm_archive
│   ├── federation/
│   │   └── client.py           # FederationClient + sync_instance
│   ├── tests/
│   │   └── test_retention.py   # Tests pytest (count / daily / GFS)
│   └── templates/
│       ├── base.html
│       ├── dashboard_local.html
│       ├── dashboard_network.html
│       ├── job_form.html
│       ├── job_history.html
│       ├── archives.html
│       ├── archives_overview.html   # Vue globale des archives
│       ├── restore_confirm.html
│       ├── settings.html
│       ├── destinations.html
│       ├── remote_instances.html
│       ├── remote_instance_form.html
│       └── federation.html
├── v3/
│   └── apps.json               # Catalogue YunoHost personnalisé (version disponible)
└── doc/

4. Modèle de données SQLite

-- Jobs de sauvegarde
CREATE TABLE jobs (
  id                   INTEGER PRIMARY KEY,
  name                 TEXT NOT NULL,
  type                 TEXT NOT NULL,        -- ynh_app|ynh_system|custom_dir|mysql|postgresql
  config_json          TEXT,
  cron_expr            TEXT NOT NULL,        -- ex: "0 3 * * 1" ou "" (manuel uniquement)
  retention_mode       TEXT NOT NULL,        -- count|daily|gfs
  retention_value      INTEGER NOT NULL,
  retention_gfs_config TEXT,                 -- JSON {"daily":N,"weekly":M,"monthly":P}
  enabled              BOOLEAN DEFAULT 1,
  core_only            BOOLEAN DEFAULT 0,
  created_at           DATETIME,
  updated_at           DATETIME
);

-- Relation many-to-many Job ↔ Destination
CREATE TABLE job_destinations (
  id          INTEGER PRIMARY KEY,
  job_id      INTEGER REFERENCES jobs(id) ON DELETE CASCADE,
  dest_type   TEXT NOT NULL,    -- ssh | instance
  dest_id     INTEGER,          -- FK → destinations.id (si ssh)
  instance_id INTEGER           -- FK → remote_instances.id (si instance)
);

-- Destinations de transfert SSH/rsync
CREATE TABLE destinations (
  id          INTEGER PRIMARY KEY,
  name        TEXT NOT NULL,
  host        TEXT NOT NULL,
  port        INTEGER DEFAULT 22,
  user        TEXT NOT NULL DEFAULT 'root',
  remote_path TEXT NOT NULL,
  key_name    TEXT,             -- fichier clé dans data_dir/keys/
  enabled     BOOLEAN DEFAULT 1,
  created_at  DATETIME
);

-- Historique des exécutions (backup ET restauration)
CREATE TABLE runs (
  id            INTEGER PRIMARY KEY,
  job_id        INTEGER REFERENCES jobs(id),
  started_at    DATETIME,
  finished_at   DATETIME,
  status        TEXT,           -- running|success|warning|error
  log_text      TEXT,           -- préfixé [RESTAURATION] si restauration
  archive_name  TEXT,
  size_bytes    INTEGER
);

-- Paramètres SMTP et notifications
CREATE TABLE settings (
  key   TEXT PRIMARY KEY,       -- smtp_host, smtp_port, smtp_user, …
  value TEXT NOT NULL DEFAULT ''
);

-- Instances distantes enregistrées (fédération)
CREATE TABLE remote_instances (
  id          INTEGER PRIMARY KEY,
  name        TEXT NOT NULL,    -- ex: "tom"
  url         TEXT NOT NULL,    -- https://tom.domaine.fr
  api_key     TEXT NOT NULL,
  last_seen   DATETIME,
  status      TEXT,             -- online|offline|error
  created_at  DATETIME
);

-- Cache états distants
CREATE TABLE remote_runs (
  id              INTEGER PRIMARY KEY,
  instance_id     INTEGER REFERENCES remote_instances(id),
  job_id          INTEGER,
  job_name        TEXT,
  job_type        TEXT,
  last_run_at     DATETIME,
  last_status     TEXT,
  last_archive_name TEXT,
  last_size_bytes INTEGER
);

-- Transferts chunked en cours
CREATE TABLE uploads (
  upload_id        TEXT PRIMARY KEY,  -- uuid4
  filename         TEXT,
  total_size       INTEGER,
  chunk_size       INTEGER,
  chunks_received  INTEGER DEFAULT 0,
  checksum         TEXT,              -- SHA256
  started_at       DATETIME,
  status           TEXT               -- pending|in_progress|complete|error
);

5. Types de jobs

ynh_app

yunohost backup create --apps <app_id> --name <archive_name>
BACKUP_CORE_ONLY=1 yunohost backup create --apps nextcloud --name <archive_name>
{ "app_id": "nocodb", "core_only": false }

Le nom du job est auto-rempli depuis le label de l'application YunoHost sélectionnée.

ynh_system

yunohost backup create --system [hook1 hook2 …] --name <archive_name>
{ "hooks": ["conf_nginx", "conf_ssowat"] }

Les hooks système disponibles sont listés dynamiquement. La liste vide déclenche une sauvegarde système complète.

custom_dir

{
  "source_path": "/opt/hermes-agent",
  "excludes": ["cache/", "logs/", "*.tmp"],
  "restore": {
    "system_user": { "name": "hermes-agent", "home": "/opt/hermes-agent", "shell": "/bin/false" },
    "systemd_service": { "name": "hermes-agent", "service_file": "/opt/hermes-agent/hermes-agent.service" },
    "permissions": { "owner": "hermes-agent:hermes-agent", "mode": "750" },
    "post_restore_commands": ["systemctl restart hermes-agent"]
  }
}

Format archive compatible YunoHost :

jerry_hermes-agent_20260508.tar
├── backup.csv              # requis YNH
├── backup_info.json        # métadonnées BackupManager
└── data/custom/opt/hermes-agent/...

jerry_hermes-agent_20260508.info.json   # SÉPARÉ hors tar, requis YNH

mysql

{ "database": "mabase", "user": "mabase_user" }

postgresql

{ "database": "mabase", "user": "postgres" }

6. Rétention

La rétention est appliquée après chaque backup réussi, localement et sur chaque destination. Les échecs de suppression sont loggés explicitement dans le run (plus de silence silencieux).

Mode count

→ Garde les N dernières archives par job
→ Supprime les plus anciennes au-delà de N

Mode daily — fenêtre glissante

retention_value: 30
→ 1 archive par jour sur les 30 derniers jours
→ Fenêtre glissante : toute archive > J-30 supprimée
→ Doublons du même jour éliminés (garde le plus récent)

Mode gfs — Grandfather-Father-Son

{ "daily": 7, "weekly": 4, "monthly": 12 }
  • Fils : conserve les N archives les plus récentes
  • Père : conserve 1 archive par semaine (la plus récente) sur M semaines
  • Grand-Père : conserve 1 archive par mois (la plus récente) sur P mois
  • Une archive peut satisfaire plusieurs niveaux simultanément

Rétention distante SSH

Après chaque transfert rsync SSH réussi, la même politique est appliquée sur le serveur distant via SSH (find pour lister, rm -f pour supprimer). Les échecs de connexion ou de suppression sont loggés sans bloquer le job.

Rétention distante instance fédérée

Après chaque push HTTP chunked, la rétention est appliquée via l'API REST de l'instance distante (DELETE /api/v1/archives/<name>).


7. Nommage des archives

Format : {instance}_{label}_{YYYYMMDD}.tar

jerry_nocodb_20260508.tar
jerry_nextcloud_20260508.tar
jerry_system_20260508.tar
jerry_hermes-agent_20260508.tar
jerry_mysql_mabase_20260508.tar

Si le nom du jour existe déjà (backup manuel + automatique le même jour), un suffixe est ajouté : jerry_nextcloud_20260508_2.tar.

backup_info.json embarqué dans le tar :

{
  "instance_name": "jerry",
  "instance_url": "https://jerry.mondomaine.fr",
  "type": "ynh_app",
  "created_at": "2026-05-08T03:00:00",
  "backupmanager_version": "1.0.0"
}

8. Destinations (many-to-many)

Un job peut avoir plusieurs destinations : aucune (local uniquement), une ou plusieurs SSH, une ou plusieurs instances fédérées, ou toute combinaison.

Après chaque backup réussi :

  1. Rétention locale appliquée
  2. Pour chaque destination dans l'ordre :
    • Transfert (rsync SSH ou HTTP chunked)
    • Rétention distante appliquée sur cette destination
    • Log intermédiaire persisté en base après chaque étape (checkpoint)

9. Intégration YunoHost

Outil YNH Usage
yunohost backup create Jobs ynh_app et ynh_system
yunohost backup restore Restauration webadmin
yunohost app list --output-as json Formulaire job
/home/yunohost.backup/archives/ Toutes les archives
Nginx + systemd + SSOwat + Let's Encrypt Infra app

SSOwat (manifest.toml) :

[resources.permissions.main]
url = "/"
allowed = "admins"

[resources.permissions.api]
url = "/api"
allowed = "visitors"    # sécurisé par token Flask
auth_header = false
protected = true

Sudoers : les commandes autorisées couvrent yunohost backup, mysqldump, pg_dump, stat, find, rsync, tar, rm, mkdir, chown, chmod, useradd, systemctl.


10. API REST

Tous les endpoints protégés par X-BackupManager-Key.

GET    /api/v1/health
GET    /api/v1/summary
GET    /api/v1/jobs
GET    /api/v1/jobs/<id>/runs
POST   /api/v1/jobs/<id>/run
GET    /api/v1/running
GET    /api/v1/archives
GET    /api/v1/archives/<name>/info
DELETE /api/v1/archives/<name>
GET    /api/v1/archives/<name>/download
GET    /api/v1/archives/<name>/info-json-download
POST   /api/v1/archives/<name>/restore
GET    /api/v1/archives/<name>/restore/status
POST   /api/v1/archives/upload/start
POST   /api/v1/archives/upload/<id>/chunk/<n>
POST   /api/v1/archives/upload/<id>/finish
DELETE /api/v1/archives/upload/<id>

11. Transfert inter-instances

HTTP Chunked (fédération)

Chunks : 50 MB | Reprise : upload_id SQLite | Vérif : SHA256 | Transport : HTTPS

SSH/rsync (destinations)

ssh-keygen -t ed25519 -f $data_dir/keys/dest_<name>_ed25519 -N "" -C "backupmanager@<dest>"
rsync -az -e "ssh -i $key -p $port" archive.tar archive.info.json user@host:/remote/path/

12. Restauration assistée

Action Dashboard BM Webadmin YNH
App YNH ✅ (via YNH) ✅ natif
Système YNH ✅ (via YNH) ✅ natif
Fichiers custom_dir ✅ complet ✅ partiel
User système useradd
Service systemd systemctl
Permissions chown/chmod
Post-restore commands
MySQL / PostgreSQL
Instance distante

13. Sécurité

Élément Mesure
Dashboard SSO YunoHost admins
Token API secrets.token_hex(32), hashé bcrypt
Clé SSH ed25519, $data_dir/keys/, permissions 600
Credentials DB Jamais en clair, config protégée (chmod 600)
Logs Sans credentials
Inter-instances HTTPS obligatoire
sudo_rm Vérifie le code retour, logue les échecs explicitement

14. Phases de développement

Phase 1 — MVP local ✅

  • manifest.toml, install, remove, nginx, systemd
  • Flask + SQLite + APScheduler
  • Jobs ynh_app et ynh_system
  • Rétention count et daily
  • Dashboard local (jobs + historique + Run now)

Phase 2 — Périmètre complet ✅ (testé VPS 2026-05-09)

  • Jobs custom_dir (exclusions + format YNH compatible)
  • Jobs mysql et postgresql
  • Restauration complète custom_dir, mysql, postgresql, ynh_app, ynh_system
  • Restaurations tracées dans l'historique du job
  • Destinations rsync SSH
  • Transfert automatique post-backup
  • Nommage unique (suffixe _2, _3… si doublon du jour)
  • Notifications email SMTP (succès/erreur)
  • Liste BDD live dans le formulaire (mysql/postgresql)
  • Accès archives root-owned via sudo (stat/find/tar/rsync)

Phase 3 — Fédération ✅ (testé VPS 2026-05-10)

  • Modèles DB RemoteInstance / RemoteRun / Upload
  • API REST complète (/summary, /archives, /restore, upload chunked, /running)
  • UI instances distantes (liste, ajout, édition, suppression, test, sync)
  • FederationClient + sync_instance
  • Dashboard réseau (vue agrégée locale + distante avec statuts)
  • Push archive HTTP chunked (sha256 + reprise upload_id)
  • Pull dernière archive d'un job distant + .info.json
  • Lancer un job sur instance distante depuis le dashboard réseau
  • Token API affiché dans les Paramètres + URL instance
  • Rétention distante après push HTTP chunked

Phase 3 bis — Refactoring & corrections ✅ (2026-05-10)

  • Découpage app.py → 5 blueprints Flask + helpers.py
  • Fix : sudo cat non autorisé → remplacé par sudo rsync → open → sudo rm
  • Fix : .info.json non rapatrié → finally avec sudo rm -rf
  • Tous les templates mis à jour (url_for('blueprint.fonction'))

Phase 4 — Refonte UI ✅ (2026-05-10)

  • Navigateur d'archives /archives — tableau filtrable, actions Restaurer · Pousser · Télécharger · Supprimer
  • Barre d'activité sticky — polling /api/v1/running toutes les 5 s, rechargement auto
  • Boutons unifiés — 4 classes CSS (btn-primary/secondary/ghost/danger)
  • Navigation simplifiée — Dashboard · Archives · Paramètres
  • Dashboard home — colonne Transfert, section Serveurs fédérés
  • Paramètres multi-onglets — Destinations · Instances · Configuration
  • Icône application — favicon + logo navbar

Phase 5 — Consolidation ✅ (2026-07-17)

  • Rétention GFS (Grandfather-Father-Son : daily/weekly/monthly)
  • Export/import configuration JSON (jobs, destinations, instances, SMTP)
  • Tests automatisés pytest — rétention count/daily/GFS (17 tests)
  • Destinations multiples par job (many-to-many : SSH + instances combinables)
  • Sélection des hooks système pour les jobs ynh_system
  • Auto-remplissage du nom depuis le label de l'app YunoHost sélectionnée
  • Logs intermédiaires persistés après chaque étape (checkpoint en base)
  • Fix rétention silencieuse : sudo_rm vérifie le code retour, échecs loggés
  • Rétention distante SSH : après chaque transfert rsync, nettoyage via SSH
  • Vue globale des archives /overview : local + SSH + instances, suppression manuelle, rétention à la demande

Phase 6 — Qualité & opérations ✅ (2026-07-17)

  • Vue globale par job avec onglets (par job / par destination) — indicateur de réplication coloré
  • Collecte parallèle des destinations dans la vue globale (ThreadPoolExecutor)
  • Fix timeout instances fédérées : (connect=5s, read=15s) au lieu de 15s global
  • Fix core_only : BACKUP_CORE_ONLY passé via env_keep sudoers (plus de sudo env)
  • Fix listage SSH : remplacement de find -printf (GNU only) par find + stat portable
  • Catalogue YunoHost personnalisé (v3/apps.json) — mises à jour détectables depuis l'interface web

En cours / À venir

  • Script backup/restore de l'app pour YNH (SQLite + clés SSH)
  • Pagination de l'historique des runs
  • Notifications Gotify / Matrix (en plus de SMTP)

15. Notes techniques

  • Threads APScheduler : app = current_app._get_current_object() avant le thread, with app.app_context() dans le thread
  • API auth : scoped au blueprint via @bp.before_request (pas de filtre global)
  • sudo rsync : crée des fichiers /tmp owned root → cleanup via sudo rm -rf (sudoers)
  • Runs bloqués : nettoyés toutes les heures par APScheduler (> 6h → status error)
  • Migration SQLite : colonnes ajoutées à chaud via ALTER TABLE dans app.py au démarrage
  • Listage SSH : cd {dir} && find . -maxdepth 1 -name '*.tar' | stat -c%s — portable (pas de find -printf absent sur BusyBox/BSD)
  • core_only : variable BACKUP_CORE_ONLY=1 transmise via subprocess(env=) + env_keep dans sudoers (pas via sudo env non autorisé)
  • Vue globale : collecte des destinations en parallèle via ThreadPoolExecutor — une destination lente ne bloque pas les autres

16. Processus de release

Configuration initiale (une seule fois par serveur YunoHost)

  1. Créer /etc/yunohost/apps_catalog.yml :

    - id: default
     url: https://app.yunohost.org/default
    - id: backupmanager-hansen
     url: https://git.hansen.tl/YunoHost-Apps/backupmanager_ynh/raw/main
    
  2. Rafraîchir le cache : sudo yunohost tools update apps

YunoHost construit l'URL du catalogue en ajoutant /v3/apps.json à l'URL de base, soit : https://git.hansen.tl/YunoHost-Apps/backupmanager_ynh/raw/main/v3/apps.json

À chaque release

  1. Bumper la version dans deux fichiers (ex : 1.1~ynh11.2~ynh1) :
    • manifest.toml → champ version
    • v3/apps.json → champ manifest.version
  2. Committer et pousser sur main
  3. YunoHost détecte la différence lors du prochain rafraîchissement (quotidien automatique ou manuel avec sudo yunohost tools update apps) et propose la mise à jour dans l'interface web
  4. L'upgrade récupère le code depuis le git et installe la nouvelle version

backupmanager_ynh — CDC v5.0 — 2026-07-17 | Phase 1 ✅ | Phase 2 ✅ | Phase 3 ✅ | Phase 3bis ✅ | Phase 4 ✅ | Phase 5 ✅