fix(test): select persistent UEFI test media without changing boot order
This commit is contained in:
+14
-4
@@ -635,7 +635,7 @@ def qemu_path(path):
|
||||
return str(path)
|
||||
|
||||
|
||||
def qemu_argv(qemu, scratch, disk, code, variables, iso, memory):
|
||||
def qemu_argv(qemu, scratch, disk, code, variables, iso, memory, *, keep_cdrom_drive=False):
|
||||
scratch = Path(scratch)
|
||||
owned_file(disk, scratch)
|
||||
owned_file(variables, scratch)
|
||||
@@ -650,8 +650,17 @@ def qemu_argv(qemu, scratch, disk, code, variables, iso, memory):
|
||||
if iso is not None:
|
||||
if not Path(iso).is_file():
|
||||
raise SmokeError('Trusted ISO must exist as a regular file')
|
||||
argv += ['-drive', f'file={qemu_path(iso)},media=cdrom,format=raw,readonly=on',
|
||||
'-boot', 'order=d,strict=on']
|
||||
if keep_cdrom_drive:
|
||||
# Keep one SATA0 device even without media so OVMF retains its DVD boot
|
||||
# entry across ISO-free boots. No file= means there is no optical medium.
|
||||
optical = 'if=ide,index=0,id=acceptance-cdrom,media=cdrom,readonly=on'
|
||||
if iso is not None:
|
||||
optical += ',format=raw,file=' + qemu_path(iso)
|
||||
argv += ['-drive', optical]
|
||||
elif iso is not None:
|
||||
argv += ['-drive', f'file={qemu_path(iso)},media=cdrom,format=raw,readonly=on']
|
||||
if iso is not None:
|
||||
argv += ['-boot', 'order=d,strict=on']
|
||||
else:
|
||||
argv += ['-boot', 'order=c,strict=on']
|
||||
return argv
|
||||
@@ -816,7 +825,8 @@ class VM:
|
||||
(scratch / name).unlink(missing_ok=True)
|
||||
self.stderr = open(scratch / f'{phase}-qemu.log', 'ab', buffering=0)
|
||||
self.process = subprocess.Popen(qemu_argv(args.qemu, scratch, disk,
|
||||
args.ovmf_code, scratch / 'OVMF_VARS.fd', iso, args.memory),
|
||||
args.ovmf_code, scratch / 'OVMF_VARS.fd', iso, args.memory,
|
||||
keep_cdrom_drive=getattr(args, 'keep_cdrom_drive', False)),
|
||||
stdin=subprocess.DEVNULL, stdout=self.stderr, stderr=self.stderr)
|
||||
self.console = None
|
||||
self.qmp = None
|
||||
|
||||
+53
-11
@@ -357,14 +357,50 @@ def probe(console, mode, context, timeout=180):
|
||||
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.
|
||||
def select_boot_entry(console, label, expected_path, create=None):
|
||||
# Harness-only one-shot selection: never rewrite BootOrder or firmware VARS.
|
||||
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]
|
||||
before = re.findall(r'^BootOrder:\s*([^\r\n]+)', output, re.M)
|
||||
if len(before) != 1:
|
||||
raise SmokeError('Expected one observable UEFI BootOrder: ' + output)
|
||||
pattern = r'^Boot([0-9A-Fa-f]{4})(\*?)[ \t]+' + re.escape(label) + r' *\t([^\r\n]+)'
|
||||
entries = re.findall(pattern, output, re.M)
|
||||
if not entries and create:
|
||||
# OVMF may prune create-only entries that are outside BootOrder after
|
||||
# consuming BootNext. Recreate only our known loader; never reorder it.
|
||||
console.command(create, 30)
|
||||
output = console.command('sudo -n efibootmgr', 30).decode()
|
||||
if re.findall(r'^BootOrder:\s*([^\r\n]+)', output, re.M) != before:
|
||||
raise SmokeError('Create-only registration changed UEFI BootOrder')
|
||||
entries = re.findall(pattern, output, re.M)
|
||||
if len(entries) != 1 or entries[0][1] != '*':
|
||||
raise SmokeError('Expected one active ' + label + ' entry: ' + output)
|
||||
entry, _, path = entries[0]
|
||||
if not re.fullmatch(expected_path, path, re.I):
|
||||
raise SmokeError('Unexpected ' + label + ' boot device or loader: ' + path)
|
||||
console.command('sudo -n efibootmgr --bootnext ' + entry, 30)
|
||||
after = console.command('sudo -n efibootmgr', 30).decode()
|
||||
if re.findall(r'^BootOrder:\s*([^\r\n]+)', after, re.M) != before:
|
||||
raise SmokeError('BootNext selection changed UEFI BootOrder')
|
||||
if re.findall(r'^BootNext:\s*([0-9A-Fa-f]{4})\s*$', after, re.M) != [entry]:
|
||||
raise SmokeError('UEFI did not retain the requested BootNext')
|
||||
return entry
|
||||
|
||||
|
||||
def select_installed(console, *, recreate=False):
|
||||
# Deliberate action AFTER proving the installer preserved the original order.
|
||||
esp = console.command('sudo -n blkid -s PARTUUID -o value /dev/vda1', 30).decode().strip()
|
||||
if not re.fullmatch(r'[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}', esp):
|
||||
raise SmokeError('Expected GPT identity of the disposable ESP')
|
||||
path = re.escape('HD(1,GPT,' + esp + ',0x800,0x100000)/\\EFI\\HydraOS\\grubx64.efi')
|
||||
create = ("sudo -n efibootmgr --create-only --disk /dev/vda --part 1 "
|
||||
"--label HydraOS --loader '\\EFI\\HydraOS\\grubx64.efi'")
|
||||
return select_boot_entry(console, 'HydraOS', path, create if recreate else None)
|
||||
|
||||
|
||||
def select_live(console):
|
||||
path = re.escape('PciRoot(0x0)/Pci(0x1f,0x2)/Sata(0,65535,0)') + r'(?:\{auto_created_boot_option\})?'
|
||||
return select_boot_entry(console, 'UEFI QEMU DVD-ROM QM00001', path)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
@@ -373,6 +409,7 @@ def main(argv=None):
|
||||
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.keep_cdrom_drive = True
|
||||
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')
|
||||
@@ -393,6 +430,9 @@ def main(argv=None):
|
||||
console = vm.console
|
||||
if live:
|
||||
console.expect(rb'hydra-live[^\r\n]*[#$] ', args.boot_timeout)
|
||||
# Keep asynchronous mount diagnostics out of framed JSON on ttyS0;
|
||||
# they remain available in the guest's dmesg/journal buffers.
|
||||
console.command('sudo -n dmesg --console-level 3', 30)
|
||||
else:
|
||||
console.expect(rb'login: ', args.boot_timeout); console.send('hydratest\n')
|
||||
console.expect(rb'Password: ', 30); console.send(password + '\n')
|
||||
@@ -431,7 +471,8 @@ def main(argv=None):
|
||||
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)
|
||||
record('signed_runtime_update_recovery', probe(console, 'runtime', context, 300))
|
||||
record('harness_checkpoint_bootnext', select_live(console)); 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()
|
||||
@@ -442,8 +483,9 @@ def main(argv=None):
|
||||
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)
|
||||
select_installed(console, recreate=True); shutdown(console)
|
||||
console = boot(False, 'installed-upgrade'); record('real_portage_upgrade_current_home', probe(console, 'upgrade', context, 720))
|
||||
record('harness_restore_bootnext', select_live(console)); 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))
|
||||
@@ -455,7 +497,7 @@ def main(argv=None):
|
||||
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)
|
||||
select_installed(console, recreate=True); 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')
|
||||
|
||||
Reference in New Issue
Block a user