Add final runtime and disk usage summary to mail reports
This commit is contained in:
1 parent
a5a7e38853
commit
be48ee85ef
13 files changed
+142
-9
No files matched your search
@@ -618,6 +618,7 @@ finish() {
|
||||
--hostname="$MAIL_HOSTNAME" --subject-prefix "$MAIL_SUBJECT_PREFIX" --run "$RUN_ID" --exit-code "$code" \
|
||||
--started "$RUN_STARTED_EPOCH" --phase "$BACKUP_PHASE" --upload-status "$UPLOAD_STATUS" \
|
||||
--config "$CONFIG_FILE" --workdir "$WORKDIR" --archive-dir "$ARCHIVE_DIR" \
|
||||
--staging-root "$STAGING_ROOT" --restore-root "$RESTORE_ROOT" --log-dir "$LOG_DIR" \
|
||||
--compression "$COMPRESSOR" --remote "$RCLONE_REMOTE_BASE" --sendmail "$MAIL_SENDMAIL_BIN" \
|
||||
--tail-lines "$MAIL_INCLUDE_LOG_TAIL_LINES" --timeout "$MAIL_SEND_TIMEOUT" 2>&1 | tee -a "$LOG_FILE"
|
||||
rm -f -- "$report_sizes"
|
||||
|
||||
+5
-7
@@ -16,6 +16,7 @@ import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from usage import human, summary
|
||||
|
||||
|
||||
def allocated(path):
|
||||
@@ -45,13 +46,6 @@ def snapshot(output, staging, archives):
|
||||
Path(output).write_text(json.dumps(data), encoding='utf-8')
|
||||
|
||||
|
||||
def human(value):
|
||||
for unit in ('B', 'KiB', 'MiB', 'GiB', 'TiB'):
|
||||
if value < 1024 or unit == 'TiB':
|
||||
return f'{value:.1f} {unit}'
|
||||
value /= 1024
|
||||
|
||||
|
||||
def deliver(args):
|
||||
fqdn = socket.getfqdn()
|
||||
host = args.hostname or fqdn
|
||||
@@ -91,6 +85,8 @@ def deliver(args):
|
||||
else:
|
||||
body.append('Archive total: '+human(sum(sizes['archives'].values())))
|
||||
body.extend(['', f'Log-Auszug (letzte {args.tail_lines} Zeilen)', '-----------------', tail])
|
||||
body.extend(summary(time.time() - args.started, args.workdir, args.archive_dir,
|
||||
args.staging_root, args.restore_root, args.log_dir))
|
||||
message = EmailMessage(policy=SMTP)
|
||||
# EmailMessage rejects CR/LF in headers and encodes non-ASCII safely.
|
||||
sender = args.sender or f'app-backup@{fqdn}'
|
||||
@@ -117,6 +113,8 @@ def main():
|
||||
snap.add_argument(name)
|
||||
mail = commands.add_parser('deliver')
|
||||
mail.add_argument('--hostname', default='')
|
||||
for name in ('staging-root', 'restore-root', 'log-dir'):
|
||||
mail.add_argument('--'+name, required=True)
|
||||
for name in ('sizes', 'log', 'output', 'to', 'sender', 'subject-prefix', 'run', 'phase',
|
||||
'upload-status', 'config', 'workdir', 'archive-dir', 'compression', 'remote', 'sendmail'):
|
||||
mail.add_argument('--'+name, required=True)
|
||||
|
||||
+5
-1
@@ -25,6 +25,7 @@ import tarfile
|
||||
import time
|
||||
|
||||
import backup_set
|
||||
from usage import summary
|
||||
|
||||
GIB = 1024 ** 3
|
||||
MARKER = 'app-backup-restore-test-v1\n'
|
||||
@@ -530,6 +531,8 @@ exec mariadbd --no-defaults --user=root --datadir=/var/lib/mysql/db --socket=/va
|
||||
f"Einträge {c.get('entries', '?')}; Tabellen {c.get('tables', '-')}; Phase {c.get('phase', '?')}")
|
||||
body.extend(['', 'Dateien entpackt; enthaltene DB-Dumps isoliert importiert und Tabellen geprüft.',
|
||||
'Dies ist kein Start-/Funktionstest der produktiven Anwendungen.'])
|
||||
body.extend(summary(time.time() - self.started, self.args.workdir, self.args.archive_dir,
|
||||
self.args.staging_root, self.args.restore_root, self.args.log_dir))
|
||||
message.set_content('\n'.join(body))
|
||||
raw = message.as_bytes()
|
||||
(self.root / 'report.eml').write_bytes(raw)
|
||||
@@ -579,7 +582,8 @@ def io_bytes(raw):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('mode', choices=('run', 'check', 'cleanup', 'prepare'))
|
||||
for key in ('workdir', 'prefix', 'remote', 'run', 'image', 'hostname', 'to', 'sender', 'sendmail', 'mail'):
|
||||
for key in ('workdir', 'archive-dir', 'staging-root', 'restore-root', 'log-dir',
|
||||
'prefix', 'remote', 'run', 'image', 'hostname', 'to', 'sender', 'sendmail', 'mail'):
|
||||
parser.add_argument('--' + key, required=True)
|
||||
for key in ('remote-check', 'config'):
|
||||
parser.add_argument('--' + key)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Best-effort, bounded disk usage summary for backup and restore mail reports."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def human(value):
|
||||
for unit in ('B', 'KiB', 'MiB', 'GiB', 'TiB'):
|
||||
if value < 1024 or unit == 'TiB':
|
||||
return f'{value:.1f} {unit}'
|
||||
value /= 1024
|
||||
|
||||
|
||||
def duration(seconds):
|
||||
hours, remainder = divmod(max(0, int(seconds)), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
return f'{hours}h {minutes:02d}m {seconds:02d}s'
|
||||
|
||||
|
||||
def allocated(path):
|
||||
"""Count allocated bytes without following symlinks or crossing filesystems."""
|
||||
try:
|
||||
result = subprocess.run(['du', '-sx', '-B1', '--', str(path)],
|
||||
capture_output=True, text=True, timeout=30, check=True)
|
||||
return int(result.stdout.split()[0])
|
||||
except (OSError, ValueError, IndexError, subprocess.SubprocessError):
|
||||
return None
|
||||
|
||||
|
||||
def free_space(path):
|
||||
try:
|
||||
stat = os.statvfs(path)
|
||||
return stat.f_bavail * stat.f_frsize
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def summary(seconds, workdir, archives, staging, restore, logs):
|
||||
"""Measure the final local state, after temporary-file cleanup."""
|
||||
lines = ['', 'Laufzeit und Speicher am Ende', '----------------------------',
|
||||
f'Verarbeitung: {duration(seconds)} ({max(0, int(seconds))}s)']
|
||||
paths = [('Arbeitsbereich gesamt', workdir), ('Backups (lokale Archive)', archives),
|
||||
('Backup-Staging', staging), ('Restore-Arbeitsbereich', restore),
|
||||
('Restore-Test', str(Path(workdir) / 'restore-test')), ('Protokolle und Berichte', logs)]
|
||||
for label, name in paths:
|
||||
if not name:
|
||||
continue
|
||||
path = Path(name)
|
||||
amount = allocated(path) if path.exists() and not path.is_symlink() else None
|
||||
lines.append(f'{label}: {human(amount) if amount is not None else "nicht verfügbar"} ({path})')
|
||||
devices = set()
|
||||
for label, name in [('Arbeitsbereich', workdir), ('Archive', archives),
|
||||
('Staging', staging), ('Restore', restore), ('Restore-Test', str(Path(workdir) / 'restore-test')),
|
||||
('Protokolle', logs)]:
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
path = Path(name)
|
||||
if path.is_symlink():
|
||||
continue
|
||||
device = path.stat().st_dev
|
||||
except OSError:
|
||||
continue
|
||||
if device in devices:
|
||||
continue
|
||||
devices.add(device)
|
||||
amount = free_space(path)
|
||||
lines.append(f'Frei auf Datenträger {label}: '
|
||||
f'{human(amount) if amount is not None else "nicht verfügbar"} ({path})')
|
||||
lines.append('Teilbereiche sind im Arbeitsbereich enthalten; getrennte Datenträger werden separat gemessen.')
|
||||
return lines
|
||||
Reference in new issue
Block a user