Skip to content

#77 — config-backup-git

PLANIFIÉ

Priorité: 🟠 HAUTE · Type: C (Celery) · Conteneur: rgz-beat · Code: app/tasks/config_backup.pyDépendances: #7 rgz-gateway


Description

Sauvegarde quotidienne à 04:00 UTC des configurations réseau vers dépôt git local (git.rgz.local). Permet :

  • Versioning: Historique complet modifications config (qui a changé quoi, quand)
  • Audit trail: Logs commits automatiques avec timestamp
  • Rollback rapide: Revenir à version précédente en < 5 min si config cassée
  • Multi-repo: Configs séparées par domaine (gateway, dns, radius, kea)

Configurations sauvegardées :

  • nftables rules (scripts/gateway/*.nft)
  • Unbound DNS config + sinkhole rules (config/unbound/)
  • FreeRADIUS config (config/radius/)
  • Kea DHCP config (config/kea/)
  • BGP/OSPF (quand implémentés)

Chaque commit = snapshot config jour précédent. En cas incident config, audit trail + rollback possible.

Architecture Interne

Flux de Backup

Quotidiennement 04:00 UTC:

Celery Beat déclenche rgz.backup.configs

Pour chaque domaine (gateway, dns, radius, kea):
  1. Récupérer fichier config actuel (de volumes Docker)
  2. Comparer avec dernière version sauvegardée
  3. SI changé:
     - Copier fichier vers git working dir
     - Ajouter à git: git add {fichier}
     - Commit avec message: "backup {timestamp} {fichier} {hash_ancien}->{hash_nouveau}"
  4. SI aucun changé pour ce domaine:
     - Commit vide (marquer date dernière sauvegarde)
  5. Git push git.rgz.local (origin)

Notification:
  Email NOC: "Config backup — 4 files changed, 2 commits"

Logging:
  Enregistrer commit hashes dans DB pour audit trail

Schéma de Données

sql
-- Table tracking git backups
TABLE config_git_backups:
  id UUID PK
  backup_date TIMESTAMP
  domain TEXT CHECK(gateway|dns|radius|kea|ospf)
  config_file TEXT (ex: /config/radius/radiusd.conf)
  commit_hash TEXT (git SHA1)
  commit_message TEXT
  changes_made BOOLEAN (true si fichier modifié)
  previous_hash TEXT (commit parent)
  backed_up_at TIMESTAMP
  backed_up_by TEXT ("system", "manual", etc.)

-- Table rollback capability
TABLE config_rollback_history:
  id UUID PK
  requested_at TIMESTAMP
  requested_by UUID FK (user qui demande rollback)
  domain TEXT
  target_hash TEXT (commit hash à restaurer)
  rollback_status CHECK(pending|in_progress|success|failed)
  completed_at TIMESTAMP
  reason TEXT (why rollback was needed)
  duration_seconds INT

Exemple Git Backup

GIT CONFIG BACKUP — February 6, 2026

Start:           2026-02-06 04:00:00 UTC
Repo:            git.rgz.local:/configs.git

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DOMAIN: gateway

Changes:
  scripts/gateway/rgz-main.nft
    Previous: a1b2c3d4e5f6 (2026-02-05)
    Current:  f7e42c9f5a3e (2026-02-06)
    Modified: +2 rules (DNS sinkhole list update)

  scripts/gateway/watch_config.sh
    Unchanged (hash: 7c3d4e5f6a7b)

Commit:  2026-02-06T04:15:23Z
  Author: celery-beat@rgz.local
  Message: "config backup 2026-02-06 scripts/gateway/rgz-main.nft a1b2c3d→f7e42c9f"
  Hash:    abc123def456

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DOMAIN: dns

Changes:
  config/unbound/unbound.conf
    Previous: 3d4f5e6a7b8c (2026-02-05)
    Current:  8c9d0e1f2a3b (2026-02-06)
    Modified: +12 new blocklist entries

  config/unbound/unbound.conf.d/sinkhole.conf
    Unchanged (hash: 2b4d7c8f6a9e)

Commit:  2026-02-06T04:22:15Z
  Hash:    ghj456khi789

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DOMAIN: radius

Changes:
  config/radius/radiusd.conf
    Unchanged (hash: 9e2b4d7c8f6a)
  config/radius/clients.conf
    Unchanged (hash: 6a9e2b4d7c8f)

Commit:  2026-02-06T04:23:00Z
  Author: celery-beat@rgz.local
  Message: "config backup 2026-02-06 — no changes (radius domain stable)"
  Hash:    lmn789opq012

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GIT PUSH: git.rgz.local (origin)
  Commits pushed:  3
  Status:         SUCCESS ✓

────────────────────────────────────────────────────────

AUDIT TRAIL (via git log):
  $ git log --oneline | head
  abc123 config backup 2026-02-06 scripts/gateway/...
  ghj456 config backup 2026-02-06 config/unbound/...
  lmn789 config backup 2026-02-06 (no changes radius)
  pqr012 config backup 2026-02-05 config/radius/...
  stu345 config backup 2026-02-04 scripts/gateway/...

ROLLBACK CAPABILITY:
  Si incident le 2026-02-06, facilement revenir:
  $ git checkout abc123^ -- config/  # Revenir à 2026-02-05
  $ docker-compose exec rgz-gateway /entrypoint.sh # Reload
  → Downtime: < 5 minutes

────────────────────────────────────────────────────────

End:      2026-02-06 04:28:47 UTC
Next:     2026-02-07 04:00:00 UTC

Configuration

env
# Config Git Backup
CONFIG_BACKUP_ENABLED=true
CONFIG_BACKUP_HOUR_UTC=4
CONFIG_BACKUP_MINUTE_UTC=0

# Git Repository
GIT_BACKUP_REPO=git@git.rgz.local:configs.git
GIT_BACKUP_BRANCH=main
GIT_SSH_KEY_PATH=/home/rgz-app/.ssh/git_rsa
GIT_SSH_KNOWN_HOSTS=/home/rgz-app/.ssh/known_hosts
GIT_WORKING_DIR=/tmp/config-backup-git
GIT_USER_NAME=celery-beat@rgz.local
GIT_USER_EMAIL=celery@rgz.local

# Domains to backup
CONFIG_BACKUP_DOMAINS=gateway,dns,radius,kea

# Paths to backup (per domain)
CONFIG_BACKUP_PATHS__gateway=scripts/gateway/*.nft,scripts/gateway/watch_config.sh
CONFIG_BACKUP_PATHS__dns=config/unbound/unbound.conf,config/unbound/unbound.conf.d/*
CONFIG_BACKUP_PATHS__radius=config/radius/radiusd.conf,config/radius/clients.conf,config/radius/dictionary
CONFIG_BACKUP_PATHS__kea=config/kea/kea-dhcp4.conf

# Retention
CONFIG_BACKUP_GIT_RETENTION_MONTHS=24  # Keep 2 years git history

# Notifications
CONFIG_BACKUP_NOTIFY_ON_CHANGES=true
CONFIG_BACKUP_NOTIFY_RECIPIENTS=noc@rgz.local

Endpoints API

MéthodeRouteDescriptionRéponse
GET/api/v1/backups/config/statusStatut dernier backup
GET/api/v1/backups/config/history?domain={dns}Historique changements configList[{date, file, commit_hash, change_type}]
GET/api/v1/backups/config/diff?commit1={hash1}&commit2={hash2}Diff entre 2 commits
POST/api/v1/backups/config/rollback?commit={hash}Rollback à config antérieure (admin)202 Accepted +
GET/api/v1/backups/config/rollback/status?task_id={id}Suivi rollback

Authentification: Admin + NOC only

Celery Task

ChampValeur
Task namergz.backup.configs
ScheduleDaily 04:00 UTC (0 4 * * *)
Queuergz.maintenance
Timeout300s
Retry2x

Logique esquisse:

python
@app.task(name='rgz.backup.configs', bind=True)
def backup_configs_to_git(self):
    """
    Backup configurations réseau vers git.rgz.local
    """
    backup_date = datetime.utcnow()
    results = {}

    try:
        # 1. Clone/fetch repo
        git_repo = _ensure_git_repo_cloned()

        # 2. Pour chaque domaine
        for domain in settings.CONFIG_BACKUP_DOMAINS:
            domain_results = {
                'domain': domain,
                'files_changed': 0,
                'files_unchanged': 0,
                'commit_hash': None
            }

            # Récupérer fichiers à backup
            file_patterns = settings.CONFIG_BACKUP_PATHS.get(domain, [])
            files_to_backup = _expand_file_patterns(file_patterns)

            any_change = False

            for src_file in files_to_backup:
                # Copier fichier vers git working dir
                dst_file = os.path.join(git_repo, src_file)
                os.makedirs(os.path.dirname(dst_file), exist_ok=True)

                # Comparer avec version précédente
                old_hash = _get_file_hash(dst_file) if os.path.exists(dst_file) else None
                new_hash = _get_file_hash(src_file)

                if old_hash != new_hash:
                    shutil.copy2(src_file, dst_file)
                    git_repo.index.add(dst_file)
                    domain_results['files_changed'] += 1
                    any_change = True

                    logger.info(f"Config changed: {src_file} ({old_hash}{new_hash})")
                else:
                    domain_results['files_unchanged'] += 1

            # 3. Commit si changement
            if any_change:
                commit_message = f"config backup {backup_date.isoformat()} {domain}"
                commit = git_repo.index.commit(
                    commit_message,
                    author=Actor(settings.GIT_USER_NAME, settings.GIT_USER_EMAIL)
                )
                domain_results['commit_hash'] = commit.hexsha
                logger.info(f"Committed {domain}: {commit.hexsha}")
            else:
                # Commit vide (just mark date)
                commit_message = f"config backup {backup_date.isoformat()} — no changes ({domain})"
                # Git allows empty commit with --allow-empty
                try:
                    commit = git_repo.index.commit(
                        commit_message,
                        author=Actor(settings.GIT_USER_NAME, settings.GIT_USER_EMAIL)
                    )
                except:
                    logger.info(f"No changes for {domain}, skipping empty commit")

            results[domain] = domain_results

        # 4. Git push
        git_repo.remotes.origin.push(settings.GIT_BACKUP_BRANCH)
        logger.info(f"Pushed {len(results)} domains to {settings.GIT_BACKUP_REPO}")

        # 5. Enregistrer metadata
        for domain, domain_result in results.items():
            backup_exec = ConfigGitBackup(
                backup_date=backup_date,
                domain=domain,
                commit_hash=domain_result['commit_hash'],
                changes_made=domain_result['files_changed'] > 0,
                backed_up_at=datetime.utcnow()
            )
            db.add(backup_exec)

        db.commit()

        # 6. Notification
        total_changed = sum(r['files_changed'] for r in results.values())
        total_commits = sum(1 for r in results.values() if r['commit_hash'])

        send_email.delay(
            to=settings.CONFIG_BACKUP_NOTIFY_RECIPIENTS,
            subject=f"Config Backup — {total_changed} files changed, {total_commits} commits",
            template='config_backup_success',
            context={'results': results}
        )

        return {
            'status': 'success',
            'results': results
        }

    except Exception as e:
        logger.error(f"Config backup failed: {e}")
        send_email.delay(
            to=settings.CONFIG_BACKUP_NOTIFY_RECIPIENTS,
            subject='ALERT: Config Backup FAILED',
            template='config_backup_failure',
            context={'error': str(e)}
        )
        self.retry(exc=e, countdown=300)

Commandes Utiles

bash
# Déclencher config backup manuellement
docker-compose exec rgz-api celery -A app.celery_app call rgz.backup.configs

# Voir historique commits config
cd /tmp/config-backup-git && git log --oneline --graph | head -20

# Diff commits (voir changements config)
cd /tmp/config-backup-git && git diff abc123 ghj456 -- config/

# Vérifier statut git repo
curl -H "Authorization: Bearer {admin_token}" \
  "http://api-rgz.duckdns.org/api/v1/backups/config/status" | jq

# Historique changements domain spécifique
curl -H "Authorization: Bearer {admin_token}" \
  "http://api-rgz.duckdns.org/api/v1/backups/config/history?domain=dns" | jq

# Rollback à version antérieure
curl -X POST -H "Authorization: Bearer {admin_token}" \
  "http://api-rgz.duckdns.org/api/v1/backups/config/rollback?commit=abc123def456"

# Logs backup
docker-compose logs rgz-beat | grep "rgz.backup.configs"

# Vérifier git repo connectivity
ssh -i /home/rgz-app/.ssh/git_rsa git@git.rgz.local "git -C /data/configs.git log --oneline | head"

Implémentation TODO

  • [ ] Schéma DB config_git_backups + config_rollback_history
  • [ ] Tâche Celery rgz.backup.configs dans app/tasks/config_backup.py
  • [ ] Fonction _ensure_git_repo_cloned() (setup git working dir)
  • [ ] Fonction _expand_file_patterns() (glob support)
  • [ ] Fonction _get_file_hash() (SHA256)
  • [ ] Endpoints API GET/POST /api/v1/backups/config/*
  • [ ] Rollback task: rgz.config.rollback (trigger + validation)
  • [ ] Git SSH key management (security, rotation)
  • [ ] Email notification templates (changes, failures)
  • [ ] Tests: simulate config changes, diff, rollback
  • [ ] Documentation: SOP config management, git workflow, emergency rollback

Dernière mise à jour: 2026-02-21

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone