65 lines
3.3 KiB
Python
65 lines
3.3 KiB
Python
"""Closed-file operator backup. Shared Deployment helpers are injected by the caller."""
|
|
|
|
|
|
def backup(deployment, output):
|
|
deployment.preflight()
|
|
if deployment.pending.exists():
|
|
raise RuntimeError('Resolve the pending deployment before creating a backup')
|
|
expected = manifest(deployment.app / 'current')
|
|
if expected != json.loads((deployment.app / 'deployed.json').read_text()):
|
|
raise RuntimeError('Current release differs from the verified deployment manifest')
|
|
state = deployment.command('systemctl', '--user', 'show', deployment.service, '--property=ActiveState', '--value')
|
|
if state not in {'active', 'inactive', 'failed'}:
|
|
raise RuntimeError('Service is transitioning; retry when its state is stable')
|
|
was_running = state == 'active'
|
|
if not was_running and deployment.command('systemctl', '--user', 'show', deployment.service, '--property=MainPID', '--value') != '0':
|
|
raise RuntimeError('Stopped service still has a process')
|
|
had_maintenance = deployment.flag.exists()
|
|
with tempfile.TemporaryDirectory(prefix='manual-backup-', dir=deployment.app) as temporary:
|
|
snapshot = Path(temporary)
|
|
(snapshot / 'release').mkdir(mode=0o700)
|
|
# Immutable release files can be copied before stopping, under the deployment lock.
|
|
for name in ('werkjournal.jar', 'manifest.json'):
|
|
shutil.copyfile(deployment.app / 'current' / name, snapshot / 'release' / name)
|
|
if not had_maintenance:
|
|
deployment.maintenance()
|
|
if was_running:
|
|
deployment.stop()
|
|
try:
|
|
shutil.copytree(deployment.app / 'data', snapshot / 'data')
|
|
files = {}
|
|
for path in sorted(snapshot.rglob('*')):
|
|
if path.is_file():
|
|
with path.open('rb') as stream:
|
|
os.fsync(stream.fileno())
|
|
files[path.relative_to(snapshot).as_posix()] = checksum(path)
|
|
atomic(snapshot / 'backup.json', json.dumps({
|
|
'schemaVersion': 1, 'application': 'werkjournal', 'createdAt': int(time.time()),
|
|
'release': expected, 'files': files}, indent=2) + '\n')
|
|
finally:
|
|
if was_running:
|
|
deployment.start()
|
|
deployment.ready(expected)
|
|
if not had_maintenance:
|
|
deployment.flag.unlink()
|
|
sync_directory(deployment.site)
|
|
# Transfer only after the former operating state has been restored.
|
|
with tarfile.open(fileobj=output, mode='w|gz') as archive:
|
|
for name in ['backup.json', *sorted(files)]:
|
|
archive.add(snapshot / name, arcname=name, recursive=False)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
os.umask(0o077)
|
|
deployment = Deployment(Path.home())
|
|
try:
|
|
with (deployment.app / '.deploy.lock').open('a') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
backup(deployment, sys.stdout.buffer)
|
|
except BlockingIOError:
|
|
print('Backup refused: another deployment or maintenance operation holds the lock.', file=sys.stderr)
|
|
raise SystemExit(1)
|
|
except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as error:
|
|
print('Backup failed: ' + str(error), file=sys.stderr)
|
|
raise SystemExit(1)
|