Estimate repair storage needs live without relying on journal history
This commit is contained in:
1 parent
d22838f818
commit
72969605fd
3 files changed
+78
-13
No files matched your search
+13
-4
@@ -204,8 +204,8 @@ nicht als automatische Löschkandidaten freigegeben.
|
|||||||
zusammen mit der ACME-Reparatur aus dem ai-rem-maintenance-Paket gestartet:
|
zusammen mit der ACME-Reparatur aus dem ai-rem-maintenance-Paket gestartet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo bash /var/tmp/install-gemini-repair-20261018.sh
|
sudo bash /var/tmp/resume-gemini-repair-20261018.sh
|
||||||
sudo journalctl -fu jr-gemini-repair-20261018.service --no-pager
|
sudo journalctl -fu jr-gemini-repair-20261018-r2.service --no-pager
|
||||||
```
|
```
|
||||||
|
|
||||||
Der einmalige Hintergrundauftrag erfordert das Betreiberpasswort. Quellen,
|
Der einmalige Hintergrundauftrag erfordert das Betreiberpasswort. Quellen,
|
||||||
@@ -235,7 +235,7 @@ Die Speicheroperation erhält die bisherigen Archive wie folgt:
|
|||||||
Prüfungen mit der vorgeschlagenen Konfiguration und geerbter echter Sperre.
|
Prüfungen mit der vorgeschlagenen Konfiguration und geerbter echter Sperre.
|
||||||
- `MIN_FREE_GB`, Aufbewahrung und Schätzung bleiben unverändert. Auf `/` verbleiben
|
- `MIN_FREE_GB`, Aufbewahrung und Schätzung bleiben unverändert. Auf `/` verbleiben
|
||||||
mindestens die konfigurierte Reserve plus 2 GiB. Auf `/var` wird Platz für das
|
mindestens die konfigurierte Reserve plus 2 GiB. Auf `/var` wird Platz für das
|
||||||
zuletzt beobachtete Snapshotbudget plus Reserve plus 10 GiB zusätzlichen
|
aktuell geschätzte Snapshotbudget plus Reserve plus 10 GiB zusätzlichen
|
||||||
Spielraum geschaffen. Die aktuelle Live-Schätzung ist trotzdem maßgeblich.
|
Spielraum geschaffen. Die aktuelle Live-Schätzung ist trotzdem maßgeblich.
|
||||||
|
|
||||||
Danach wird der normale `app-backup.service` gestartet. Der erfolgreiche
|
Danach wird der normale `app-backup.service` gestartet. Der erfolgreiche
|
||||||
@@ -249,6 +249,15 @@ Konfiguration allein stellt verlagerten Bestand und freien Platz nicht zurück;
|
|||||||
kein blindes Zurückkopieren oder Löschen. Den Nachweis pro Objekt zur gezielten
|
kein blindes Zurückkopieren oder Löschen. Den Nachweis pro Objekt zur gezielten
|
||||||
Wiederherstellung beziehungsweise Auswahl historischer Restore-Quellen nutzen.
|
Wiederherstellung beziehungsweise Auswahl historischer Restore-Quellen nutzen.
|
||||||
|
|
||||||
Sieben lokale Tests prüfen Datei- und Verzeichnisverlagerung, Kopierkorruption
|
Acht lokale Tests prüfen Datei- und Verzeichnisverlagerung, Kopierkorruption
|
||||||
mit erhaltenem Original, Schutz vor Quellenüberschneidung/Links und Platzreserve.
|
mit erhaltenem Original, Schutz vor Quellenüberschneidung/Links und Platzreserve.
|
||||||
Die produktive Abnahme und ein vollständiger Restore-Test stehen weiterhin aus.
|
Die produktive Abnahme und ein vollständiger Restore-Test stehen weiterhin aus.
|
||||||
|
|
||||||
|
### Fortsetzung nach Certbot-Abbruch
|
||||||
|
|
||||||
|
Der erste Auftrag vom 18.10.2026 scheiterte bei Certbot, bevor die
|
||||||
|
Speicheroperation begann. Revision 2 schätzt den aktuellen Bedarf lesend aus
|
||||||
|
rsync-Dateistatistiken mit denselben Excludes und aus DB-Größen. Dateimetadaten,
|
||||||
|
SQL-Ausdehnung und Wachstumsreserve entsprechen dem bestehenden Backup-Programm.
|
||||||
|
Ältere Journalzeilen werden dafür nicht benötigt; die vorhandenen echten
|
||||||
|
`--prepare-space`-/`--check`-Prüfungen bleiben die abschließende Voraussetzung.
|
||||||
@@ -178,6 +178,61 @@ printf '%s\\0' "$LOCKFILE" "$WORKDIR" "$STAGING_ROOT" "$ARCHIVE_DIR" "$MIN_FREE_
|
|||||||
return fields[:6], fields[6:]
|
return fields[:6], fields[6:]
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_budget(file_stats, database_sizes):
|
||||||
|
total = 0
|
||||||
|
for size, count in file_stats:
|
||||||
|
if size < 0 or count < 0:
|
||||||
|
raise ValueError('Invalid file statistics')
|
||||||
|
total += size + count * 8192
|
||||||
|
for size in database_sizes:
|
||||||
|
if size < 0:
|
||||||
|
raise ValueError('Invalid database size')
|
||||||
|
total += size * 3 + 67108864
|
||||||
|
return total * 12 // 10 + 67108864
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_budget():
|
||||||
|
# Same dry-run exclusions, file metadata, SQL expansion and growth allowance
|
||||||
|
# as installed lib/space.sh. No cleanup, write, service stop or journal needed.
|
||||||
|
script = r'''set -e
|
||||||
|
LIB_DIR=/usr/local/lib/app-backup
|
||||||
|
source "$1"
|
||||||
|
CONFIG_FILE=$2
|
||||||
|
load_config
|
||||||
|
build_plan
|
||||||
|
guard_workspaces
|
||||||
|
for i in "${!LABELS[@]}"; do
|
||||||
|
check_source_path "${SOURCES[i]}"
|
||||||
|
exclusions "${LABELS[i]}"
|
||||||
|
stats=$(rsync -aHAX --dry-run --stats --numeric-ids "${EXCLUDES[@]}" -- "${SOURCES[i]}/" "$STAGING_ROOT/.space-estimate/" 8>&- 9>&-)
|
||||||
|
bytes=$(awk '/^Total file size:/ {gsub(/,/, "", $4); print $4}' <<< "$stats")
|
||||||
|
count=$(awk '/^Number of files:/ {gsub(/,/, "", $4); print $4}' <<< "$stats")
|
||||||
|
[[ $bytes =~ ^[0-9]+$ && $count =~ ^[0-9]+$ ]] || exit 1
|
||||||
|
printf 'F %s %s\n' "$bytes" "$count"
|
||||||
|
done
|
||||||
|
for i in "${!DB_NAMES[@]}"; do
|
||||||
|
secure_file "${DB_CNFS[i]}"
|
||||||
|
size=$("$DB_CLIENT_BIN" --defaults-extra-file="${DB_CNFS[i]}" --database="${DB_NAMES[i]}" --batch --skip-column-names --execute='SELECT COALESCE(SUM(data_length+index_length),0) FROM information_schema.tables WHERE table_schema=DATABASE()')
|
||||||
|
[[ $size =~ ^[0-9]+$ ]] || exit 1
|
||||||
|
printf 'D %s\n' "$size"
|
||||||
|
done
|
||||||
|
'''
|
||||||
|
result = subprocess.run(['bash', '-c', script, 'estimate', str(COMMON), str(CONFIG)],
|
||||||
|
check=True, capture_output=True, text=True, pass_fds=(9,), timeout=900)
|
||||||
|
files, databases = [], []
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if re.fullmatch(r'F [0-9]+ [0-9]+', line):
|
||||||
|
_, size, count = line.split()
|
||||||
|
files.append((int(size), int(count)))
|
||||||
|
elif re.fullmatch(r'D [0-9]+', line):
|
||||||
|
databases.append(int(line.split()[1]))
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Unexpected source estimation output')
|
||||||
|
if not files:
|
||||||
|
raise RuntimeError('No source file estimates')
|
||||||
|
return snapshot_budget(files, databases)
|
||||||
|
|
||||||
|
|
||||||
def free(path):
|
def free(path):
|
||||||
v = os.statvfs(path)
|
v = os.statvfs(path)
|
||||||
return v.f_bavail * v.f_frsize
|
return v.f_bavail * v.f_frsize
|
||||||
@@ -233,15 +288,10 @@ def main():
|
|||||||
private_dir(path)
|
private_dir(path)
|
||||||
if STAGING.stat().st_dev == archives.stat().st_dev or PRESERVED.stat().st_dev == archives.stat().st_dev:
|
if STAGING.stat().st_dev == archives.stat().st_dev or PRESERVED.stat().st_dev == archives.stat().st_dev:
|
||||||
raise SystemExit('Required separate filesystems are absent')
|
raise SystemExit('Required separate filesystems are absent')
|
||||||
# The unchanged real estimator is run again below before activating config.
|
budget = estimate_budget()
|
||||||
journal = subprocess.run(['journalctl', '-u', 'app-backup.service', '--since', '2026-10-18',
|
print('Live snapshot budget: ' + str(budget) + ' bytes', flush=True)
|
||||||
'--no-pager', '-o', 'cat'], check=True, capture_output=True, text=True)
|
|
||||||
budgets = re.findall(r'Estimated snapshot budget: ([0-9]+) bytes', journal.stdout)
|
|
||||||
if not budgets:
|
|
||||||
raise SystemExit('No observed snapshot budget; inspect before relocation')
|
|
||||||
budget = int(budgets[-1])
|
|
||||||
if free(STAGING) < budget + reserve:
|
if free(STAGING) < budget + reserve:
|
||||||
raise SystemExit('Staging filesystem lacks observed budget plus unchanged reserve')
|
raise SystemExit('Staging filesystem lacks live budget plus unchanged reserve')
|
||||||
original = CONFIG.read_bytes()
|
original = CONFIG.read_bytes()
|
||||||
saved = STATE / 'app-backup.conf.before'
|
saved = STATE / 'app-backup.conf.before'
|
||||||
if not saved.exists():
|
if not saved.exists():
|
||||||
@@ -265,7 +315,7 @@ def main():
|
|||||||
budget + reserve + 10 * GIB, reserve + 2 * GIB)
|
budget + reserve + 10 * GIB, reserve + 2 * GIB)
|
||||||
json_atomic(STATE / 'relocation-plan.json', {
|
json_atomic(STATE / 'relocation-plan.json', {
|
||||||
'staging': str(STAGING), 'active_archives': str(archives),
|
'staging': str(STAGING), 'active_archives': str(archives),
|
||||||
'preservation_root': str(PRESERVED), 'observed_snapshot_budget': budget,
|
'preservation_root': str(PRESERVED), 'live_snapshot_budget': budget,
|
||||||
'moves': [{'source': str(p), 'allocated_bytes': n} for p, n in moves]})
|
'moves': [{'source': str(p), 'allocated_bytes': n} for p, n in moves]})
|
||||||
for src, amount in moves:
|
for src, amount in moves:
|
||||||
if free(PRESERVED) < amount + reserve + 2 * GIB:
|
if free(PRESERVED) < amount + reserve + 2 * GIB:
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ m = importlib.util.module_from_spec(spec)
|
|||||||
spec.loader.exec_module(m)
|
spec.loader.exec_module(m)
|
||||||
|
|
||||||
class PreservationTests(unittest.TestCase):
|
class PreservationTests(unittest.TestCase):
|
||||||
|
def test_live_budget_preserves_file_metadata_sql_and_growth_allowances(self):
|
||||||
|
raw=100 + 2*8192 + 200*3 + 67108864
|
||||||
|
self.assertEqual(m.snapshot_budget([(100,2)],[200]),raw*12//10+67108864)
|
||||||
|
with self.assertRaises(ValueError):m.snapshot_budget([(-1,2)],[])
|
||||||
|
with self.assertRaises(ValueError):m.snapshot_budget([],[-1])
|
||||||
|
|
||||||
def test_plan_keeps_root_reserve_and_stops_when_var_target_is_met(self):
|
def test_plan_keeps_root_reserve_and_stops_when_var_target_is_met(self):
|
||||||
self.assertEqual(m.choose_moves([('a', 48), ('b', 7), ('c', 7), ('d', 7)], 26, 77, 84, 12), [('a',48),('b',7),('c',7)])
|
self.assertEqual(m.choose_moves([('a', 48), ('b', 7), ('c', 7), ('d', 7)], 26, 77, 84, 12), [('a',48),('b',7),('c',7)])
|
||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(RuntimeError):
|
||||||
|
|||||||
Reference in new issue
Block a user