#!/usr/bin/env python3 """Real laptop acceptance in a NEW disposable UEFI/KVM VM; retains scratch evidence. Only ISO/firmware inputs are accepted. Never attach host devices or existing VM images. A private HTTP server exposes exact TEST artifact bytes, never a directory. """ from __future__ import annotations import argparse import hashlib import http.server import importlib.util import io import json import os from pathlib import Path import re import secrets import shlex import shutil import stat import subprocess import tempfile import threading import time import zipfile _ROOT = Path(__file__).resolve().parents[1] _spec = importlib.util.spec_from_file_location('hydra_iso_smoke', _ROOT / 'dev/iso-smoke.py') iso_smoke = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(iso_smoke) SmokeError = iso_smoke.SmokeError def argument_parser(): parser = argparse.ArgumentParser(description=__doc__) for option in ('iso', 'ovmf-code', 'ovmf-vars'): parser.add_argument('--' + option, required=True, type=Path) parser.add_argument('--qemu', default='qemu-system-x86_64') parser.add_argument('--qemu-img', default='qemu-img') parser.add_argument('--scratch-parent', type=Path, default=Path(tempfile.gettempdir())) parser.add_argument('--memory', type=int, default=4096) parser.add_argument('--boot-timeout', type=int, default=360) parser.add_argument('--install-timeout', type=int, default=1800) return parser def verify_preserved(before, after): for name in ('gpt', 'foreign_sha256', 'foreign_efi', 'boot_order'): if before[name] != after[name]: raise SmokeError('Preserved installation changed ' + name) def partition_installer_command(token): if not re.fullmatch('[a-zA-Z0-9]+', token): raise SmokeError('Invalid installer token') return ('sudo -n hydra-install --root-partition /dev/vda3 --esp-partition /dev/vda1 ' '--root-fs btrfs --user hydratest --execute --confirm-format /dev/vda3 ' '--register-boot-entry; rc=$?; ' f"printf '\\036{token}:INSTALL_EXIT:%s\\037' \"$rc\"") def artifact_server(files): payload = dict(files) class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): data = payload.get(self.path) if data is None: self.send_error(404) return self.send_response(200) self.send_header('Content-Type', 'application/octet-stream') self.send_header('Content-Length', str(len(data))) self.end_headers() self.wfile.write(data) def log_message(self, *args): pass return http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler) def test_artifacts(scratch, runtime, commit): """Generate disposable TEST trust; never include private bytes in the share.""" private = scratch / 'TEST-private.pem' public = scratch / 'public.pem' subprocess.run(['openssl', 'genpkey', '-algorithm', 'ED25519', '-out', str(private)], check=True, capture_output=True, timeout=15) private.chmod(0o600) shared = {} try: subprocess.run(['openssl', 'pkey', '-in', str(private), '-pubout', '-out', str(public)], check=True, capture_output=True, timeout=15) shared['/public.pem'] = public.read_bytes() for sequence, name in ((1, 'one'), (2, 'two')): path = 'usr/lib/hydra/hydra/cli.py' content = runtime + ('\n# Disposable laptop acceptance TEST sequence ' + str(sequence) + '\n').encode() stream = io.BytesIO() with zipfile.ZipFile(stream, 'w', zipfile.ZIP_DEFLATED) as archive: info = zipfile.ZipInfo(path, (1980, 1, 1, 0, 0, 0)) info.create_system = 3 info.external_attr = (stat.S_IFREG | 0o644) << 16 info.compress_type = zipfile.ZIP_DEFLATED archive.writestr(info, content) payload = stream.getvalue() digest = lambda data: hashlib.sha256(data).hexdigest() manifest = {'format': 1, 'product': 'Hydra OS', 'scope': 'runtime', 'channel': 'stable', 'architecture': 'x86_64', 'sequence': sequence, 'version': '0.0.0-test-' + str(sequence), 'source_commit': commit, 'archive': {'name': 'runtime.zip', 'size': len(payload), 'sha256': digest(payload)}, 'files': {path: {'size': len(content), 'sha256': digest(content), 'mode': 0o644}}} raw = (json.dumps(manifest, sort_keys=True, separators=(',', ':')) + '\n').encode() input_path, signature = scratch / 'TEST-manifest.json', scratch / 'TEST-manifest.sig' input_path.write_bytes(raw) subprocess.run(['openssl', 'pkeyutl', '-sign', '-rawin', '-inkey', str(private), '-in', str(input_path), '-out', str(signature)], check=True, capture_output=True, timeout=15) for file, data in (('manifest.json', raw), ('manifest.sig', signature.read_bytes()), ('runtime.zip', payload)): shared['/' + name + '/' + file] = data input_path.unlink(); signature.unlink() finally: private.unlink(missing_ok=True) return shared GUEST_COMMON = r''' import contextlib, hashlib, json, os, re, runpy, shutil, signal, subprocess, sys, time, urllib.request from pathlib import Path def run(*args, timeout=120): p = subprocess.run(args, capture_output=True, text=True, timeout=timeout) if p.returncode: raise RuntimeError('command failed: ' + repr(args) + '\n' + p.stdout + p.stderr) return p.stdout def sha(path): h = hashlib.sha256() with open(path, 'rb') as source: for block in iter(lambda: source.read(1024 * 1024), b''): h.update(block) return h.hexdigest() def tree(path): return {str(p.relative_to(path)): sha(p) for p in sorted(path.rglob('*')) if p.is_file()} @contextlib.contextmanager def mounted(device, options=''): import tempfile with tempfile.TemporaryDirectory(prefix='hydra-acceptance-') as directory: args = ['mount'] + (['-o', options] if options else []) + [device, directory] run(*args) try: yield Path(directory) finally: run('umount', directory) def preserved(): table = json.loads(run('sfdisk', '--json', '/dev/vda'))['partitiontable'] # No filesystem UUIDs: selected root UUID must change on formatting. with mounted('/dev/vda1', 'ro') as esp: foreign = {k: v for k, v in tree(esp / 'EFI').items() if not k.startswith('HydraOS/')} order = re.search(r'^BootOrder:\s*(.*)$', run('efibootmgr'), re.M) assert order, 'UEFI BootOrder was not observable' return {'gpt': table, 'foreign_sha256': sha('/dev/vda2'), 'foreign_efi': foreign, 'boot_order': order.group(1) if order else None} def base_state(root, esp): pkg = root / 'var/db/pkg/app-misc' records = {p.name: tree(p) for p in sorted(pkg.glob('hydra-acceptance-*')) if p.is_dir()} return {'boot': {k: v for k, v in tree(root / 'boot').items() if not k.startswith('efi/')}, 'portage_fixture': records, 'world': sha(root / 'var/lib/portage/world'), 'owned_efi': tree(esp / 'EFI/HydraOS'), 'fixture': (root / 'usr/share/hydra/base-fixture.txt').read_text()} def fixture(version): repo = Path('/var/lib/hydra-acceptance-repo') (repo / 'profiles').mkdir(parents=True, exist_ok=True) (repo / 'metadata').mkdir(exist_ok=True) (repo / 'profiles/repo_name').write_text('hydra-acceptance\n') (repo / 'metadata/layout.conf').write_text('masters =\nauto-sync = false\nthin-manifests = true\n') conf = Path('/etc/portage/repos.conf') conf.mkdir(exist_ok=True) (conf / 'hydra-acceptance.conf').write_text('[hydra-acceptance]\nlocation = ' + str(repo) + '\nauto-sync = no\n') package = repo / 'app-misc/hydra-acceptance' package.mkdir(parents=True, exist_ok=True) ebuild = package / ('hydra-acceptance-' + version + '.ebuild') ebuild.write_text('EAPI=8\nDESCRIPTION="Offline recovery acceptance fixture"\nHOMEPAGE="https://example.invalid"\nLICENSE="MIT"\nSLOT="0"\nKEYWORDS="amd64"\nS="${WORKDIR}"\nsrc_install() {\n insinto /usr/share/hydra\n printf "%s\\n" "${PV}" > base-fixture.txt\n doins base-fixture.txt\n}\n') run('unshare', '--net', '--', 'ebuild', str(ebuild), 'manifest') output = run('unshare', '--net', '--', 'emerge', '--ignore-default-opts', '--nodeps', '--autounmask=n', '--usepkg=n', '--buildpkg=n', '--getbinpkg=n', '=app-misc/hydra-acceptance-' + version, timeout=600) assert Path('/usr/share/hydra/base-fixture.txt').read_text().strip() == version assert Path('/var/db/pkg/app-misc/hydra-acceptance-' + version).is_dir() return output def stop_child(source): ready = Path('/run/hydra-acceptance-child-ready') ready.unlink(missing_ok=True) process = subprocess.Popen([sys.executable, '-I', '-c', source], stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: deadline = time.monotonic() + 120 while not ready.exists() and process.poll() is None and time.monotonic() < deadline: time.sleep(.05) if not ready.exists(): raise RuntimeError('child never reached durable mutation: ' + repr(process.communicate(timeout=5))) process.kill() process.communicate(timeout=10) assert process.returncode == -signal.SIGKILL finally: if process.poll() is None: process.kill(); process.communicate(timeout=10) ready.unlink(missing_ok=True) ''' GUEST_BODIES = { 'seed': r''' # Guest-only target is the new empty disk attached by this harness. assert os.geteuid() == 0 and Path('/etc/hydra/live-mode/active').exists() assert not Path('/sys/class/block/vda/partition').exists() assert json.loads(run('lsblk', '-J', '-o', 'NAME,TYPE,MOUNTPOINT', '/dev/vda'))['blockdevices'][0].get('children') is None run('parted', '-s', '/dev/vda', 'mklabel', 'gpt', 'mkpart', 'ESP', 'fat32', '1MiB', '513MiB', 'set', '1', 'esp', 'on', 'mkpart', 'foreign', '513MiB', '577MiB', 'mkpart', 'HydraRoot', '577MiB', '100%') run('udevadm', 'settle') run('mkfs.vfat', '-F', '32', '/dev/vda1') with open('/dev/vda2', 'r+b', buffering=0) as foreign: foreign.write((context['sentinel'] * 1024).encode()); os.fsync(foreign.fileno()) with mounted('/dev/vda1') as esp: for name in ('EFI/ForeignOS/loader.efi', 'EFI/BOOT/BOOTX64.EFI'): path = esp / name; path.parent.mkdir(parents=True, exist_ok=True) path.write_text(context['sentinel'] + ':' + name) run('sync') result = preserved() ''', 'preserved': 'result = preserved()\n', 'baseline': r''' assert not Path('/etc/hydra/live-mode/active').exists() output = fixture('1.0') home = Path('/home/hydratest') (home / 'acceptance-home').write_text('before-' + context['sentinel']) vm = home / '.local/share/hydra/acceptance-guest' vm.mkdir(parents=True, exist_ok=True) run('qemu-img', 'create', '-f', 'qcow2', str(vm / 'disk.qcow2'), '64M') (vm / 'guest-data-sentinel').write_text('VM-DATA-' + context['sentinel']) (vm / 'profile.json').write_text('{"fixture":"preserve"}\n') run('chown', '-R', 'hydratest:hydratest', str(vm)) run('chown', 'hydratest:hydratest', str(home / 'acceptance-home')) result = {'fixture_emerge': output, 'vm_data': tree(vm), 'doctor': json.loads(run('hydra', 'doctor', '--json'))} ''', 'upgrade': r''' output = fixture('2.0') Path('/home/hydratest/acceptance-home').write_text('current-' + context['sentinel']) with Path('/var/lib/portage/world').open('a') as world: world.write('# acceptance post-checkpoint world\n') result = {'fixture_emerge': output, 'home': Path('/home/hydratest/acceptance-home').read_text()} ''', 'base-state': r''' with mounted('/dev/vda3', 'ro,subvol=@root') as root: with mounted('/dev/vda1', 'ro') as esp: result = base_state(root, esp) ''', 'damage': r''' with mounted('/dev/vda3', 'subvol=@root') as root: kernels = list((root / 'boot').glob('vmlinuz*')) assert kernels, 'No actual installed kernel to damage' for kernel in kernels: kernel.write_bytes(b'DELIBERATE ACCEPTANCE KERNEL DAMAGE\n') with mounted('/dev/vda1') as esp: loader = esp / 'EFI/HydraOS/grubx64.efi' assert loader.is_file(); loader.write_bytes(b'DELIBERATE ACCEPTANCE EFI DAMAGE\n') run('sync') result = {'damaged_kernels': [p.name for p in kernels], 'damaged_owned_loader': 'EFI/HydraOS/grubx64.efi'} ''', 'restored': r''' marker = json.loads(Path('/etc/hydra/storage.json').read_text()) assert marker['root_subvolume'] == '@root' assert Path('/home/hydratest/acceptance-home').read_text() == 'current-' + context['sentinel'] assert not Path('/var/db/pkg/app-misc/hydra-acceptance-2.0').exists() assert Path('/usr/share/hydra/base-fixture.txt').read_text().strip() == '1.0' with mounted('/dev/vda1', 'ro') as esp: result = base_state(Path('/'), esp) result['home'] = Path('/home/hydratest/acceptance-home').read_text() result['vm_data'] = tree(Path('/home/hydratest/.local/share/hydra/acceptance-guest')) result['root_mount'] = run('findmnt', '-n', '-o', 'SOURCE,OPTIONS', '/') ''', 'runtime': r''' base = Path('/var/lib/hydra-acceptance-runtime') base.mkdir(mode=0o700) url = 'http://10.0.2.2:' + str(context['port']) def download(path): with urllib.request.urlopen(url + path, timeout=15) as response: return response.read(16 * 1024 * 1024) for name in ('one', 'two'): directory = base / name; directory.mkdir() for file in ('manifest.json', 'manifest.sig', 'runtime.zip'): (directory / file).write_bytes(download('/' + name + '/' + file)) target = Path('/usr/lib/hydra/hydra/cli.py') original = target.read_bytes() key = Path('/etc/hydra/update.pub'); production_key = key.read_bytes() vm = Path('/home/hydratest/.local/share/hydra/acceptance-guest'); original_vm = tree(vm) key.write_bytes(download('/public.pem')); key.chmod(0o644) try: checked = json.loads(run('hydra', 'update', 'check', '--artifacts', str(base / 'one'))) assert checked['available']['sequence'] == 1 applied = json.loads(run('hydra', 'update', 'apply', '--artifacts', str(base / 'one'))) assert target.read_bytes() != original rollback = json.loads(run('/usr/bin/hydra-update', 'rollback', applied['transaction'])) assert target.read_bytes() == original tamper = base / 'tampered'; shutil.copytree(base / 'two', tamper) with (tamper / 'runtime.zip').open('ab') as file: file.write(b'TAMPER') prior_status = run('/usr/bin/hydra-update', 'status') refused = subprocess.run(['/usr/bin/hydra-update', 'apply', '--artifacts', str(tamper)], capture_output=True, text=True) assert refused.returncode != 0 and target.read_bytes() == original assert run('/usr/bin/hydra-update', 'status') == prior_status source = """import os, runpy, signal from pathlib import Path m = runpy.run_path('/usr/lib/hydra-update/engine.py') write = m['atomic_write'] def pause(path, *args, **kwargs): write(path, *args, **kwargs) if path == Path('/usr/lib/hydra/hydra/cli.py'): Path('/run/hydra-acceptance-child-ready').touch() os.kill(os.getpid(), signal.SIGSTOP) m['Updater'].apply.__globals__['atomic_write'] = pause m['Updater']().apply(Path(ARTIFACTS)) """.replace('ARTIFACTS', repr(str(base / 'two'))) stop_child(source) assert json.loads(run('/usr/bin/hydra-update', 'status'))['pending'] target.write_bytes(b'DELIBERATELY INVALID PYTHON !!!\n') recovered = json.loads(run('/usr/bin/hydra-update-recover')) assert target.read_bytes() == original status = json.loads(run('/usr/bin/hydra-update', 'status')) assert not status['pending'] and status['highest_sequence'] == 2 assert tree(vm) == original_vm run('hydra', 'doctor', '--json') result = {'signed_check': checked, 'apply': applied, 'rollback': rollback, 'tamper_refusal': refused.stderr, 'fresh_process_recovery': recovered, 'status': status, 'unchanged_vm_data': original_vm, 'trust': 'disposable TEST key only'} finally: key.write_bytes(production_key); key.chmod(0o644) ''', 'restore-interrupt': r''' # Instrument a real live CLI process, after its durable journal and real rename. # No fake filesystems or production fault hooks are involved. source = """import os, signal, sys from pathlib import Path sys.path.insert(0, '/usr/lib/hydra') from hydra import system_recovery as recovery save = recovery.RecoverySession.save_journal def pause(self, value, phase): save(self, value, phase) if phase == 'root-retained': Path('/run/hydra-acceptance-child-ready').touch() os.kill(os.getpid(), signal.SIGSTOP) recovery.RecoverySession.save_journal = pause raise SystemExit(recovery.main(ARGUMENTS)) """.replace('ARGUMENTS', repr(['restore', '--root-uuid', context['root_uuid'], '--checkpoint', context['checkpoint'], '--execute', '--confirm-root-uuid', context['root_uuid']])) stop_child(source) with mounted('/dev/vda3', 'ro,subvolid=5') as top: journal = json.loads((top / '@snapshots/restore.json').read_text()) assert journal['phase'] == 'root-retained' assert not (top / '@root').exists() and (top / journal['failed_root']).exists() result = {'sigkill_after_real_rename': True, 'durable_journal': journal} ''', } def guest_script(mode, context): return GUEST_COMMON + '\ncontext = ' + repr(context) + '\n' + GUEST_BODIES[mode] + '\nprint(json.dumps(result, sort_keys=True))\n' def probe(console, mode, context, timeout=180): source = guest_script(mode, context) return json.loads(console.command("sudo -n python3 - <<'HYDRA_LAPTOP_PROBE'\n" + source + '\nHYDRA_LAPTOP_PROBE', timeout)) def select_installed(console): # This is a deliberate harness action AFTER proving installer preserved order. output = console.command('sudo -n efibootmgr', 30).decode() entries = re.findall(r'^Boot([0-9A-Fa-f]{4})\*?\s+HydraOS\s', output, re.M) if len(entries) != 1: raise SmokeError('Expected one registered HydraOS entry: ' + output) console.command('sudo -n efibootmgr --bootnext ' + entries[0], 30) return entries[0] def main(argv=None): parser = argument_parser(); args = parser.parse_args(argv) for path in (args.iso, args.ovmf_code, args.ovmf_vars): if not path.is_file(): parser.error('Missing regular input: ' + str(path)) if args.memory < 2048 or min(args.boot_timeout, args.install_timeout) <= 0: parser.error('Memory >=2048 MiB and positive timeouts required') args.iso, args.ovmf_code, args.ovmf_vars = (p.resolve() for p in (args.iso, args.ovmf_code, args.ovmf_vars)) scratch = Path(tempfile.mkdtemp(prefix='hydra-laptop-smoke-', dir=args.scratch_parent)) if len(str(scratch / 'serial.sock').encode()) >= 104: parser.error('Use shorter scratch parent such as /tmp') print('Acceptance artifacts: ' + str(scratch), flush=True) inputs = {str(p): iso_smoke.digest(p) for p in (args.iso, args.ovmf_code, args.ovmf_vars)} report = {'schema': 1, 'status': 'running', 'inputs_sha256': inputs, 'checks': {}, 'physical_hardware': 'pending', 'kernel_scope': 'exact kernel restoration; no two-kernel upgrade claim', 'trust_scope': 'disposable TEST key temporarily pinned only in this disposable installed guest'} context = {'sentinel': secrets.token_hex(32)} password = secrets.token_urlsafe(24); vm = None; server = None def record(name, evidence): report['checks'][name] = evidence (scratch / 'report.json').write_text(json.dumps(report, indent=2) + '\n') print('Passed: ' + name, flush=True) def boot(live, phase): nonlocal vm vm = iso_smoke.VM(args, scratch, disk, args.iso if live else None, password, phase) console = vm.console if live: console.expect(rb'hydra-live[^\r\n]*[#$] ', args.boot_timeout) else: console.expect(rb'login: ', args.boot_timeout); console.send('hydratest\n') console.expect(rb'Password: ', 30); console.send(password + '\n') console.expect(rb'hydratest[^\r\n]*[#$] ', 60) iso_smoke.installed_admin(console, password) return console def shutdown(console): nonlocal vm console.send('sudo -n poweroff\n'); vm.wait_poweroff(90); vm.close(); vm = None try: disk = scratch / 'fresh-laptop.qcow2' iso_smoke.reserve_disk(disk) subprocess.run([args.qemu_img, 'create', '-f', 'qcow2', str(disk), '40G'], check=True, timeout=30, stdout=subprocess.DEVNULL) # Both code and variables copied; original firmware never attached writable. for source, name in ((args.ovmf_code, 'OVMF_CODE.fd'), (args.ovmf_vars, 'OVMF_VARS.fd')): shutil.copyfile(source, scratch / name); (scratch / name).chmod(0o600) args.ovmf_code = scratch / 'OVMF_CODE.fd' commit = subprocess.run(['git', '-C', str(_ROOT), 'rev-parse', 'HEAD'], check=True, capture_output=True, text=True).stdout.strip() files = test_artifacts(scratch, (_ROOT / 'hydra/cli.py').read_bytes(), commit) server = artifact_server(files); context['port'] = server.server_port threading.Thread(target=server.serve_forever, daemon=True).start() console = boot(True, 'live-install') before = probe(console, 'seed', context); record('seeded_foreign_invariants', before) token = secrets.token_hex(12); console.buffer = b'' console.send(partition_installer_command(token) + '\n') console.expect_installer(rb'Installed account password: ', 120, token); console.send(password + '\n') console.expect_installer(rb'Repeat password: ', 30, token); console.send(password + '\n') console.expect_installer(rb'Installation completed\. Diagnostic log: [^\r\n]+', args.install_timeout, token) console.finish_installer(token) after = probe(console, 'preserved', context); verify_preserved(before, after) record('preserved_partition_install', after) record('harness_bootnext_selection', select_installed(console)); shutdown(console) console = boot(False, 'installed-baseline') # The existing UX probe runs as the normal account. console.send('exit\n'); console.expect(rb'hydratest[^\r\n]*[#$] ', 30) record('packaged_ascii_branding', json.loads(iso_smoke.installed_terminal_ux_probe(console))) iso_smoke.installed_admin(console, password) baseline = probe(console, 'baseline', context, 720); record('real_portage_baseline_doctor_vmdata', baseline) record('signed_runtime_update_recovery', probe(console, 'runtime', context, 300)); shutdown(console) console = boot(True, 'live-checkpoint') base_before = probe(console, 'base-state', context); record('base_before_checkpoint', base_before) context['root_uuid'] = console.command('sudo -n blkid -s UUID -o value /dev/vda3').decode().strip() esp_partuuid = console.command('sudo -n blkid -s PARTUUID -o value /dev/vda1').decode().strip() command = ('sudo -n hydra-system checkpoint --root-uuid ' + shlex.quote(context['root_uuid']) + ' --esp-partuuid ' + shlex.quote(esp_partuuid) + ' --name laptop-before-upgrade') record('checkpoint_plan', json.loads(console.command(command, 180))) cp = json.loads(console.command(command + ' --execute --confirm-root-uuid ' + shlex.quote(context['root_uuid']), 300)) context['checkpoint'] = cp['id']; record('real_offline_checkpoint', cp) record('checkpoint_list', json.loads(console.command('sudo -n hydra-system list --root-uuid ' + shlex.quote(context['root_uuid']), 180))) select_installed(console); shutdown(console) console = boot(False, 'installed-upgrade'); record('real_portage_upgrade_current_home', probe(console, 'upgrade', context, 720)); shutdown(console) console = boot(True, 'live-restore') record('deliberate_kernel_efi_damage', probe(console, 'damage', context)) record('restore_sigkill_real_btrfs', probe(console, 'restore-interrupt', context, 300)) restore = ('sudo -n hydra-system restore --root-uuid ' + shlex.quote(context['root_uuid']) + ' --checkpoint ' + shlex.quote(context['checkpoint']) + ' --execute --confirm-root-uuid ' + shlex.quote(context['root_uuid'])) record('fresh_process_restore_resume', json.loads(console.command(restore, 300))) base_after = probe(console, 'base-state', context) if base_after != base_before: raise SmokeError('Offline restore did not restore exact base/Portage/kernel/ownedEFI hashes') preserved_after = probe(console, 'preserved', context); verify_preserved(before, preserved_after) record('restored_base_foreign_invariants', {'base': base_after, 'preserved': preserved_after}) select_installed(console); shutdown(console) console = boot(False, 'installed-restored'); restored = probe(console, 'restored', context) if any(restored[k] != v for k, v in base_before.items()): raise SmokeError('Disk-only boot did not retain restored base') if restored['vm_data'] != baseline['vm_data']: raise SmokeError('VM disk/profile bytes changed') record('disk_only_restored_boot_current_home_vmdata', restored); shutdown(console) if any(iso_smoke.digest(Path(path)) != digest for path, digest in inputs.items()): raise SmokeError('Input ISO/firmware changed') report['status'] = 'passed'; return 0 except (SmokeError, OSError, ValueError, subprocess.SubprocessError, iso_smoke.QMPError) as exc: report['status'] = 'failed'; report['error'] = str(exc).replace(password, '[REDACTED]') print('Acceptance failed: ' + report['error'], flush=True); return 1 finally: if vm: vm.close() if server: server.shutdown(); server.server_close() report['completed_utc'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) (scratch / 'report.json').write_text(json.dumps(report, indent=2) + '\n') print('Report: ' + str(scratch / 'report.json'), flush=True) if __name__ == '__main__': raise SystemExit(main())