| 1 | #!/usr/bin/env python3 |
| 2 | import importlib |
| 3 | import json |
| 4 | from pathlib import Path |
| 5 | import sqlite3 |
| 6 | import tempfile |
| 7 | |
| 8 | |
| 9 | def main(): |
| 10 | data = importlib.import_module('data') |
| 11 | with tempfile.TemporaryDirectory(prefix='dashboard-backup-') as temporary: |
| 12 | data.STATE = Path(temporary) |
| 13 | data.BACKUPS = data.STATE / 'backups' |
| 14 | live = data.STATE / 'dashboard' |
| 15 | live.mkdir() |
| 16 | connections = [] |
| 17 | for name in ['accounts.sqlite', 'connections.sqlite']: |
| 18 | db = sqlite3.connect(live / name) |
| 19 | db.execute('PRAGMA journal_mode=WAL') |
| 20 | db.execute('CREATE TABLE durable (value TEXT)') |
| 21 | db.execute('INSERT INTO durable VALUES (?)', (name,)) |
| 22 | db.commit() |
| 23 | connections.append(db) |
| 24 | (live / 'pictures').mkdir() |
| 25 | (live / 'pictures' / 'avatar').write_bytes(b'original picture') |
| 26 | backup_id = '20261005T000000Z-abcdef' |
| 27 | directory = data.BACKUPS / backup_id |
| 28 | directory.mkdir(parents=True) |
| 29 | files = data.backup_dashboard(directory / 'dashboard') |
| 30 | assert set(files) == {'accounts.sqlite', 'connections.sqlite', 'pictures/avatar'} |
| 31 | for name in ['accounts.sqlite', 'connections.sqlite']: |
| 32 | with sqlite3.connect(directory / 'dashboard' / name) as db: |
| 33 | assert db.execute('SELECT value FROM durable').fetchall() == [(name,)] |
| 34 | revision = 'a' * 16 |
| 35 | (directory / 'manifest.json').write_text(json.dumps({'id': backup_id, 'fromRelease': revision, 'services': {'dashboard': {'files': files}}})) |
| 36 | data.current_release = lambda: Path('/releases') / revision |
| 37 | lifecycle = [] |
| 38 | def run(*args, **kwargs): |
| 39 | lifecycle.append(args) |
| 40 | if args == ('systemctl', 'stop', 'studio-dashboard'): |
| 41 | for db in connections: |
| 42 | db.close() |
| 43 | data.run = run |
| 44 | for db in connections: |
| 45 | db.execute('DELETE FROM durable') |
| 46 | db.execute("INSERT INTO durable VALUES ('later change')") |
| 47 | db.commit() |
| 48 | (live / 'pictures' / 'avatar').write_bytes(b'later picture') |
| 49 | data.restore(backup_id, 'dashboard') |
| 50 | assert lifecycle == [('systemctl', 'stop', 'studio-dashboard'), ('systemctl', 'start', 'studio-dashboard'), ('systemctl', 'is-active', '--quiet', 'studio-dashboard')] |
| 51 | assert (live / 'pictures' / 'avatar').read_bytes() == b'original picture' |
| 52 | for name in ['accounts.sqlite', 'connections.sqlite']: |
| 53 | with sqlite3.connect(live / name) as db: |
| 54 | assert db.execute('SELECT value FROM durable').fetchall() == [(name,)] |
| 55 | safety = next(data.BACKUPS.glob('before-restore-*/dashboard/' + name)) |
| 56 | with sqlite3.connect(safety) as db: |
| 57 | assert db.execute('SELECT value FROM durable').fetchall() == [('later change',)] |
| 58 | lifecycle.clear() |
| 59 | (directory / 'dashboard' / 'pictures/avatar').write_bytes(b'corrupted backup') |
| 60 | try: |
| 61 | data.restore(backup_id, 'dashboard') |
| 62 | except ValueError: |
| 63 | pass |
| 64 | else: |
| 65 | raise AssertionError('corrupt backup accepted') |
| 66 | assert not lifecycle |
| 67 | print(json.dumps({'live_wal_backup': 'passed', 'account_and_connection_restore': 'passed', 'safety_copy': 'passed', 'corrupt_backup_refused_before_stop': 'passed'})) |
| 68 | |
| 69 | |
| 70 | if __name__ == '__main__': |
| 71 | main() |