#!/usr/bin/env python3 """Create a disconnected, mount-free disposable fixture; retain every attempt.""" import hashlib import json import os from pathlib import Path import shutil import subprocess import sys import time source = Path(__file__).resolve().parents[2] report = Path(sys.argv[1]).resolve() assert not report.exists() and report.parent.is_dir() report.mkdir(mode=0o700) name = 'public-worker-' + report.name assert all(c.isalnum() or c == '-' for c in name) immutable_d09 = os.environ.get('ARCHY_TEST_IMMUTABLE_D09') == '1' def run(*args, **kwargs): result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs) with (report / 'commands.log').open('ab') as log: log.write(('COMMAND ' + ' '.join(map(str, args)) + '\n').encode()) log.write(result.stdout) if result.returncode: raise RuntimeError(f'command failed ({result.returncode}): {args}; retained {report}') return result.stdout files = ['scripts/public_update_prepare.py', 'scripts/public_update_runtime.py', 'scripts/public_update_adopt.py', 'scripts/public_update_worker.py', 'scripts/tests/public_update_fixture.c', 'scripts/tests/public_update_worker_pid1.py'] digests = {} for relative in files: target = report / Path(relative).name shutil.copyfile(source / relative, target) digests[relative] = hashlib.sha256(target.read_bytes()).hexdigest() (report / 'source-sha256.json').write_text(json.dumps(digests, indent=2)) if immutable_d09: commit = run('git', '-C', str(source), 'rev-parse', 'v2.0.3-alpha^{commit}').decode().strip() assert commit == '1a4ea15df9c8800b6d288277d2839c7b93662ff7' (report / 'immutable-source-commit').write_text(commit) run('git', '-C', str(source), 'archive', '--format=tar', '--output=' + str(report / 'immutable-source.tar'), commit, 'scripts', 'image-recipe/configs', 'tests/regression/d09-systemd.py', 'tests/regression/d09-systemd-reboot.py') else: for name_part, version, extra in [('old', '1.9.1-alpha', []), ('new', '2.0.4-alpha', []), ('bad', '2.0.4-alpha', ['-DFAIL_START']), ('health', '2.0.4-alpha', ['-DHEALTH_VERSION="wrong-fixture-version"'])]: run('cc', '-static', '-O2', '-Wall', '-Wextra', f'-DVERSION="{version}"', *extra, str(report / 'public_update_fixture.c'), '-o', str(report / (name_part + '-backend'))) created = False engine = ['podman'] extra = [] try: image = run('podman', 'image', 'inspect', 'localhost/archipelago-rootfs:latest', '--format', '{{.Id}}').decode().strip() (report / 'image-id').write_text(image) if sys.argv[2:] == ['--rootful']: parent = Path(Path('/proc/self/cgroup').read_text().strip().split(':', 2)[2]).parent assert parent.name.startswith('public-worker-fixture-') and parent.suffix == '.service' store = report.parent / 'public-worker-store' run('sudo', '-n', 'install', '-d', '-m', '0700', str(store)) engine = ['sudo', '-n', 'podman', '--root=' + str(store / 'root'), '--runroot=' + str(store / 'run'), '--storage-driver=overlay', '--cgroup-manager=cgroupfs', '--events-backend=file'] if subprocess.run([*engine, 'image', 'exists', image]).returncode: run('podman', 'save', '--format', 'oci-archive', '--output', str(report / 'base-image.tar'), image) run(*engine, 'load', '--input', str(report / 'base-image.tar')) extra = ['--cgroup-parent=' + str(parent), '--security-opt=apparmor=unconfined'] run(*engine, 'run', '--detach', '--tty', '--pull=never', '--name', name, *extra, '--hostname', 'd09-qualification' if immutable_d09 else 'public-ota-worker-fixture', '--network=none', '--cgroupns=private', '--systemd=always', '--cap-add=SYS_ADMIN', '--cap-add=SYS_PTRACE', '--security-opt=unmask=ALL', '--memory=1g', '--memory-swap=1g', '--cpus=1', '--pids-limit=256', '--tmpfs', '/run', '--tmpfs', '/tmp', '--entrypoint', '/sbin/init', image, '--unit=basic.target', '--log-target=console') created = True if immutable_d09: run(*engine, 'exec', name, 'mkdir', '-p', '/opt/d09-fixture') run(*engine, 'cp', str(report / 'immutable-source.tar'), name + ':/opt/d09-fixture/source.tar') run(*engine, 'exec', name, 'tar', '-xf', '/opt/d09-fixture/source.tar', '-C', '/opt/d09-fixture') run(*engine, 'exec', '--env', 'D09_DISPOSABLE_SYSTEMD=1', name, 'python3', '/opt/d09-fixture/tests/regression/d09-systemd.py', timeout=900) else: run(*engine, 'exec', name, 'mkdir', '-p', '/opt/public-fixture') for filename in ['public_update_prepare.py', 'public_update_runtime.py', 'public_update_adopt.py', 'public_update_worker.py', 'public_update_worker_pid1.py', 'old-backend', 'new-backend', 'bad-backend', 'health-backend']: run(*engine, 'cp', str(report / filename), name + ':/opt/public-fixture/' + filename) run(*engine, 'exec', '--env', 'ARCHY_PUBLIC_WORKER_PID1=1', '--env', 'ARCHY_PUBLIC_WORKER_RUNTIME_ONLY=' + os.environ.get('ARCHY_PUBLIC_WORKER_RUNTIME_ONLY', ''), name, '/usr/bin/python3', '-B', '/opt/public-fixture/public_update_worker_pid1.py', timeout=900) run(*engine, 'restart', '--time', '10', name) for _ in range(120): probe = subprocess.run([*engine, 'exec', name, 'systemctl', 'show', '-p', 'Version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) with (report / 'reboot-bus-readiness.log').open('ab') as stream: stream.write(probe.stdout) if probe.returncode == 0: break time.sleep(.25) else: raise RuntimeError('disposable PID1 bus did not become ready after restart') if immutable_d09: run(*engine, 'exec', '--env', 'D09_DISPOSABLE_SYSTEMD=1', name, 'python3', '/opt/d09-fixture/tests/regression/d09-systemd-reboot.py', timeout=180) else: run(*engine, 'exec', name, 'systemctl', 'start', 'public-fixture-frontend.service', 'archipelago.service') run(*engine, 'exec', name, 'python3', '-I', '/opt/public-fixture/after-reboot.py', timeout=90) (report / 'result').write_text('PASS\n') finally: if created: for command, output in [([*engine, 'exec', name, 'journalctl', '--no-pager'], 'journal.log'), ([*engine, 'logs', name], 'container.log')]: result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) (report / output).write_bytes(result.stdout) retained = (['/var/lib/archipelago-d09', '/opt/d09-fixture/external-history'] if immutable_d09 else ['/var/lib/public-worker-evidence', '/var/lib/archipelago-public-transactions']) for path in retained: subprocess.run([*engine, 'cp', name + ':' + path, str(report / Path(path).name)]) subprocess.run([*engine, 'stop', '--time', '10', name], stdout=subprocess.DEVNULL) if sys.argv[2:] == ['--rootful']: run('sudo', '-n', 'chown', '-R', f'{os.getuid()}:{os.getgid()}', str(report)) print(report)