Files
Hydra-OS/dev/laptop-smoke.py
T

478 lines
26 KiB
Python
Executable File

#!/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, filesystem, options=''):
import tempfile
with tempfile.TemporaryDirectory(prefix='hydra-acceptance-') as directory:
args = ['mount', '-t', filesystem] + (['-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', 'vfat', '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', 'vfat') 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', 'btrfs', 'ro,subvol=@root') as root:
with mounted('/dev/vda1', 'vfat', 'ro') as esp:
result = base_state(root, esp)
''',
'damage': r'''
with mounted('/dev/vda3', 'btrfs', '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', 'vfat') 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', 'vfat', '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', 'btrfs', '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())