Sauvegarde Home Assistant : configuree, poussee sur deux autres machines, verifiee
Aucune sauvegarde HA n avait jamais ete faite, et git ne la couvrait pas -- un seul fichier HA y etait versionne. HA declarait pourtant trois destinations Synology, dont 10.10.10.22 et une troisieme machine en 10.10.10.50 absente de toute la documentation du projet. Quotidienne 04:45, sept copies, base d historique exclue (20 Mo au lieu de 4,4 Go), poussee sur les trois machines. La configuration passe par le websocket car l API REST ne l expose pas. Verifiee archive en main : .storage, packages, dashboards, secrets et custom_components presents, base d historique absente comme voulu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
134
tools/ha-backup-config.mjs
Normal file
134
tools/ha-backup-config.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Configure la sauvegarde automatique de Home Assistant (installation Core /
|
||||
* Container, HA 2026.8.1 — pas de Supervisor ici).
|
||||
*
|
||||
* POURQUOI CE SCRIPT PLUTÔT QUE L'INTERFACE
|
||||
* La configuration de sauvegarde n'est PAS exposée par l'API REST : elle vit
|
||||
* dans `.storage/backup` et ne se règle que par l'interface ou par le
|
||||
* websocket. Passer par le websocket la rend reproductible, versionnée, et
|
||||
* relisible dans six mois — ce qu'un clic dans l'interface n'est pas.
|
||||
*
|
||||
* CE QUI EST RÉGLÉ, ET POURQUOI
|
||||
* - Destinations : les agents Synology **ARCHIVE2 (10.10.10.22)** et
|
||||
* **ARCHIVE (10.10.10.50)**, plus une copie locale. Le NAS principal
|
||||
* (ARCHIVE3, 10.10.10.17) est délibérément EXCLU comme destination : une
|
||||
* sauvegarde qui ne survit pas à la panne qu'elle couvre ne sert à rien.
|
||||
* - Base de données EXCLUE. Ce qu'il faut pouvoir restaurer vite, c'est
|
||||
* l'identité du système (entités, helpers, cartes, paquets, intégrations) :
|
||||
* environ 60 Mo. L'historique, lui, pèse 4,4 Go, change en permanence, et
|
||||
* n'a aucune valeur d'urgence — il se sauvegarde séparément, plus rarement,
|
||||
* et avec les précautions SQLite (mode WAL : jamais un `cp` à chaud).
|
||||
* - Rétention 7 copies : une semaine pour s'apercevoir d'une bêtise.
|
||||
* - Pas de mot de passe. Choix assumé : ces archives restent sur le réseau
|
||||
* local du propriétaire, et une sauvegarde restaurable vaut mieux qu'une
|
||||
* sauvegarde chiffrée dont la clé est perdue le jour où elle sert.
|
||||
*
|
||||
* Usage :
|
||||
* node tools/ha-backup-config.mjs --dry-run (affiche avant/après, n'écrit rien)
|
||||
* node tools/ha-backup-config.mjs (applique)
|
||||
* node tools/ha-backup-config.mjs --run-now (applique puis déclenche une sauvegarde)
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const HA_URL = process.env.HA_URL || 'http://10.10.10.17:8123';
|
||||
const ARGS = new Set(process.argv.slice(2));
|
||||
const DRY = ARGS.has('--dry-run');
|
||||
const RUN_NOW = ARGS.has('--run-now');
|
||||
|
||||
function loadToken() {
|
||||
if (process.env.HA_TOKEN) return process.env.HA_TOKEN;
|
||||
const m = fs.readFileSync(path.join(ROOT, '.env'), 'utf8').match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/m);
|
||||
if (!m) { console.error('HA_TOKEN absent de .env'); process.exit(1); }
|
||||
return m[1].replace(/^["']|["']$/g, '');
|
||||
}
|
||||
|
||||
// Agents de destination. Identifiants relevés via `backup/agents/info` et
|
||||
// croisés avec les entrées `synology_dsm` de `.storage/core.config_entries`
|
||||
// pour connaître l'hôte réel de chacun — le nom seul ne dit pas où il écrit.
|
||||
const AGENTS = {
|
||||
local: 'backup.local', // 10.10.10.17, restauration rapide
|
||||
archive2: 'synology_dsm.1660NZN396007', // 10.10.10.22 — second Synology
|
||||
archive: 'synology_dsm.14B0MPN914514', // 10.10.10.50 — troisième machine
|
||||
// 'synology_dsm.20A0SBRVM9PF2' = ARCHIVE3 = 10.10.10.17 : NE PAS UTILISER
|
||||
// comme destination, c'est la machine dont on se protège.
|
||||
};
|
||||
|
||||
const DESIRED = {
|
||||
create_backup: {
|
||||
agent_ids: [AGENTS.local, AGENTS.archive2, AGENTS.archive],
|
||||
include_database: false,
|
||||
include_folders: null,
|
||||
include_all_addons: false,
|
||||
include_addons: null,
|
||||
name: null,
|
||||
password: null,
|
||||
},
|
||||
retention: { copies: 7, days: null },
|
||||
schedule: { recurrence: 'daily', time: '04:45:00', days: [] },
|
||||
};
|
||||
|
||||
const token = loadToken();
|
||||
const ws = new WebSocket(HA_URL.replace(/^http/, 'ws') + '/api/websocket');
|
||||
let seq = 1;
|
||||
const pending = new Map();
|
||||
|
||||
function call(msg) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = seq++;
|
||||
pending.set(id, { resolve, reject });
|
||||
ws.send(JSON.stringify({ id, ...msg }));
|
||||
});
|
||||
}
|
||||
|
||||
ws.onmessage = async (ev) => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.type === 'auth_required') return ws.send(JSON.stringify({ type: 'auth', access_token: token }));
|
||||
if (m.type === 'auth_invalid') { console.error('jeton HA refusé'); process.exit(1); }
|
||||
if (m.type === 'auth_ok') return main().catch(e => { console.error(e.message); process.exit(1); });
|
||||
if (m.type === 'result') {
|
||||
const p = pending.get(m.id); pending.delete(m.id);
|
||||
if (!p) return;
|
||||
m.success ? p.resolve(m.result) : p.reject(new Error(JSON.stringify(m.error)));
|
||||
}
|
||||
};
|
||||
ws.onerror = () => { console.error(`websocket injoignable sur ${HA_URL}`); process.exit(1); };
|
||||
|
||||
async function main() {
|
||||
const before = (await call({ type: 'backup/config/info' })).config;
|
||||
console.log('=== AVANT ===');
|
||||
console.log(` configurée : ${before.automatic_backups_configured}`);
|
||||
console.log(` récurrence : ${before.schedule.recurrence} à ${before.schedule.time ?? '—'}`);
|
||||
console.log(` destinations : ${before.create_backup.agent_ids.join(', ') || '— aucune —'}`);
|
||||
console.log(` base incluse : ${before.create_backup.include_database}`);
|
||||
console.log(` rétention : ${before.retention.copies ?? '—'} copies`);
|
||||
|
||||
console.log('\n=== APRÈS (souhaité) ===');
|
||||
console.log(` récurrence : ${DESIRED.schedule.recurrence} à ${DESIRED.schedule.time}`);
|
||||
console.log(' destinations :');
|
||||
console.log(` ${AGENTS.local} → 10.10.10.17 (copie locale, restauration rapide)`);
|
||||
console.log(` ${AGENTS.archive2} → 10.10.10.22 (second Synology)`);
|
||||
console.log(` ${AGENTS.archive} → 10.10.10.50 (troisième machine)`);
|
||||
console.log(` base incluse : ${DESIRED.create_backup.include_database} (les 4,4 Go d'historique se sauvegardent à part)`);
|
||||
console.log(` rétention : ${DESIRED.retention.copies} copies`);
|
||||
|
||||
if (DRY) { console.log('\n[DRY-RUN] aucune écriture.'); ws.close(); process.exit(0); }
|
||||
|
||||
await call({ type: 'backup/config/update', ...DESIRED });
|
||||
const after = (await call({ type: 'backup/config/info' })).config;
|
||||
console.log(`\n✅ appliqué — configurée=${after.automatic_backups_configured}, ` +
|
||||
`prochaine=${after.next_automatic_backup ?? '?'}`);
|
||||
|
||||
if (RUN_NOW) {
|
||||
console.log('\nDéclenchement d\'une sauvegarde de vérification…');
|
||||
await call({ type: 'backup/generate_with_automatic_settings' });
|
||||
console.log(' demandée. Suivre avec: node tools/ha-backup-list.mjs');
|
||||
}
|
||||
ws.close(); process.exit(0);
|
||||
}
|
||||
|
||||
setTimeout(() => { console.error('délai dépassé'); process.exit(1); }, 60000);
|
||||
Reference in New Issue
Block a user