added meta date for backups
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Dark-mode control plane for Incus VM backups using ZFS block devices, Restic, and S3-compatible storage.
|
Dark-mode control plane for Incus VM backups using ZFS block devices, Restic, and S3-compatible storage.
|
||||||
|
|
||||||
|
Backups include the instance disk or dataset plus Incus metadata captured with `incus config show`, `incus config show --expanded`, `incus info`, and snapshot listing. VM disks are stored as `.raw`; containers are stored as ZFS send streams (`.zfs`).
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `management/`: Central Express API with cookie login, SQLite storage, node registry, central schedules, and proxy calls to node agents.
|
- `management/`: Central Express API with cookie login, SQLite storage, node registry, central schedules, and proxy calls to node agents.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
import { finished } from 'node:stream/promises';
|
import { finished } from 'node:stream/promises';
|
||||||
import { setTimeout as delay } from 'node:timers/promises';
|
import { setTimeout as delay } from 'node:timers/promises';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
@@ -86,8 +89,12 @@ export async function runBackupJob(job, instance = null) {
|
|||||||
setJobProgress(job, { percent: 93 });
|
setJobProgress(job, { percent: 93 });
|
||||||
await deleteIncusSnapshotWithRetry(job, snapshotName);
|
await deleteIncusSnapshotWithRetry(job, snapshotName);
|
||||||
|
|
||||||
|
setJobStep(job, 'Backing up Incus metadata');
|
||||||
|
setJobProgress(job, { percent: 95 });
|
||||||
|
await backupInstanceMetadata(job, 'virtual-machine');
|
||||||
|
|
||||||
setJobStep(job, 'Applying Restic retention policy');
|
setJobStep(job, 'Applying Restic retention policy');
|
||||||
setJobProgress(job, { percent: 96 });
|
setJobProgress(job, { percent: 97 });
|
||||||
await runRestic(retentionArgs(job.vmName), {
|
await runRestic(retentionArgs(job.vmName), {
|
||||||
log: (line) => appendJobLog(job, line),
|
log: (line) => appendJobLog(job, line),
|
||||||
});
|
});
|
||||||
@@ -142,8 +149,12 @@ async function runContainerBackupJob(job) {
|
|||||||
setJobProgress(job, { percent: 93 });
|
setJobProgress(job, { percent: 93 });
|
||||||
await deleteIncusSnapshotWithRetry(job, snapshotName);
|
await deleteIncusSnapshotWithRetry(job, snapshotName);
|
||||||
|
|
||||||
|
setJobStep(job, 'Backing up Incus metadata');
|
||||||
|
setJobProgress(job, { percent: 95 });
|
||||||
|
await backupInstanceMetadata(job, 'container');
|
||||||
|
|
||||||
setJobStep(job, 'Applying Restic retention policy');
|
setJobStep(job, 'Applying Restic retention policy');
|
||||||
setJobProgress(job, { percent: 96 });
|
setJobProgress(job, { percent: 97 });
|
||||||
await runRestic(retentionArgs(job.vmName), {
|
await runRestic(retentionArgs(job.vmName), {
|
||||||
log: (line) => appendJobLog(job, line),
|
log: (line) => appendJobLog(job, line),
|
||||||
});
|
});
|
||||||
@@ -173,6 +184,46 @@ async function zfsDatasetUsed(dataset) {
|
|||||||
return Number.isFinite(size) && size > 0 ? size : null;
|
return Number.isFinite(size) && size > 0 ? size : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function backupInstanceMetadata(job, instanceType) {
|
||||||
|
const tempDir = await mkdtemp(path.join(tmpdir(), `incus-backup-${job.vmName}-`));
|
||||||
|
try {
|
||||||
|
const metadataDir = path.join(tempDir, job.vmName);
|
||||||
|
await writeMetadataFile(metadataDir, 'README.txt', metadataReadme(job.vmName, instanceType));
|
||||||
|
await writeCommandOutput(metadataDir, 'config.yaml', ['config', 'show', job.vmName]);
|
||||||
|
await writeCommandOutput(metadataDir, 'config-expanded.yaml', ['config', 'show', job.vmName, '--expanded']);
|
||||||
|
await writeCommandOutput(metadataDir, 'info.txt', ['info', job.vmName]);
|
||||||
|
await writeCommandOutput(metadataDir, 'snapshots.json', ['snapshot', 'list', job.vmName, '--format', 'json']);
|
||||||
|
await runRestic(['backup', metadataDir, '--tag', job.vmName, '--tag', 'metadata', '--tag', instanceType], {
|
||||||
|
log: (line) => appendJobLog(job, line),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeCommandOutput(directory, filename, args) {
|
||||||
|
const result = await spawnCommand('incus', args);
|
||||||
|
await writeMetadataFile(directory, filename, result.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeMetadataFile(directory, filename, content) {
|
||||||
|
const { mkdir } = await import('node:fs/promises');
|
||||||
|
await mkdir(directory, { recursive: true });
|
||||||
|
await writeFile(path.join(directory, filename), content || '', { mode: 0o600 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataReadme(instanceName, instanceType) {
|
||||||
|
return [
|
||||||
|
`Instance: ${instanceName}`,
|
||||||
|
`Type: ${instanceType}`,
|
||||||
|
`Created: ${new Date().toISOString()}`,
|
||||||
|
'',
|
||||||
|
'This directory contains Incus metadata captured alongside the disk/dataset backup.',
|
||||||
|
'It is intended for disaster-recovery reconstruction and restore preflight workflows.',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
function waitForProcess(child, label) {
|
function waitForProcess(child, label) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
child.on('error', reject);
|
child.on('error', reject);
|
||||||
|
|||||||
Reference in New Issue
Block a user