fix(test): select persistent UEFI test media without changing boot order

This commit is contained in:
Codex
2026-10-13 22:49:33 +03:00
parent 3c15890823
commit 6058a0660e
5 changed files with 185 additions and 15 deletions
+14 -4
View File
@@ -635,7 +635,7 @@ def qemu_path(path):
return str(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) scratch = Path(scratch)
owned_file(disk, scratch) owned_file(disk, scratch)
owned_file(variables, 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 iso is not None:
if not Path(iso).is_file(): if not Path(iso).is_file():
raise SmokeError('Trusted ISO must exist as a regular file') raise SmokeError('Trusted ISO must exist as a regular file')
argv += ['-drive', f'file={qemu_path(iso)},media=cdrom,format=raw,readonly=on', if keep_cdrom_drive:
'-boot', 'order=d,strict=on'] # 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: else:
argv += ['-boot', 'order=c,strict=on'] argv += ['-boot', 'order=c,strict=on']
return argv return argv
@@ -816,7 +825,8 @@ class VM:
(scratch / name).unlink(missing_ok=True) (scratch / name).unlink(missing_ok=True)
self.stderr = open(scratch / f'{phase}-qemu.log', 'ab', buffering=0) self.stderr = open(scratch / f'{phase}-qemu.log', 'ab', buffering=0)
self.process = subprocess.Popen(qemu_argv(args.qemu, scratch, disk, 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) stdin=subprocess.DEVNULL, stdout=self.stderr, stderr=self.stderr)
self.console = None self.console = None
self.qmp = None self.qmp = None
+53 -11
View File
@@ -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)) return json.loads(console.command("sudo -n python3 - <<'HYDRA_LAPTOP_PROBE'\n" + source + '\nHYDRA_LAPTOP_PROBE', timeout))
def select_installed(console): def select_boot_entry(console, label, expected_path, create=None):
# This is a deliberate harness action AFTER proving installer preserved order. # Harness-only one-shot selection: never rewrite BootOrder or firmware VARS.
output = console.command('sudo -n efibootmgr', 30).decode() output = console.command('sudo -n efibootmgr', 30).decode()
entries = re.findall(r'^Boot([0-9A-Fa-f]{4})\*?\s+HydraOS\s', output, re.M) before = re.findall(r'^BootOrder:\s*([^\r\n]+)', output, re.M)
if len(entries) != 1: if len(before) != 1:
raise SmokeError('Expected one registered HydraOS entry: ' + output) raise SmokeError('Expected one observable UEFI BootOrder: ' + output)
console.command('sudo -n efibootmgr --bootnext ' + entries[0], 30) pattern = r'^Boot([0-9A-Fa-f]{4})(\*?)[ \t]+' + re.escape(label) + r' *\t([^\r\n]+)'
return entries[0] 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): 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 not path.is_file(): parser.error('Missing regular input: ' + str(path))
if args.memory < 2048 or min(args.boot_timeout, args.install_timeout) <= 0: if args.memory < 2048 or min(args.boot_timeout, args.install_timeout) <= 0:
parser.error('Memory >=2048 MiB and positive timeouts required') 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)) 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)) 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') 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 console = vm.console
if live: if live:
console.expect(rb'hydra-live[^\r\n]*[#$] ', args.boot_timeout) 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: else:
console.expect(rb'login: ', args.boot_timeout); console.send('hydratest\n') console.expect(rb'login: ', args.boot_timeout); console.send('hydratest\n')
console.expect(rb'Password: ', 30); console.send(password + '\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))) record('packaged_ascii_branding', json.loads(iso_smoke.installed_terminal_ux_probe(console)))
iso_smoke.installed_admin(console, password) iso_smoke.installed_admin(console, password)
baseline = probe(console, 'baseline', context, 720); record('real_portage_baseline_doctor_vmdata', baseline) 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') console = boot(True, 'live-checkpoint')
base_before = probe(console, 'base-state', context); record('base_before_checkpoint', base_before) 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() 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)) 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) 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))) 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) select_installed(console, recreate=True); shutdown(console)
console = boot(False, 'installed-upgrade'); record('real_portage_upgrade_current_home', probe(console, 'upgrade', context, 720)); 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') console = boot(True, 'live-restore')
record('deliberate_kernel_efi_damage', probe(console, 'damage', context)) record('deliberate_kernel_efi_damage', probe(console, 'damage', context))
record('restore_sigkill_real_btrfs', probe(console, 'restore-interrupt', context, 300)) 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') 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) preserved_after = probe(console, 'preserved', context); verify_preserved(before, preserved_after)
record('restored_base_foreign_invariants', {'base': base_after, 'preserved': 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) 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 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') if restored['vm_data'] != baseline['vm_data']: raise SmokeError('VM disk/profile bytes changed')
+10
View File
@@ -24,6 +24,16 @@ adds the following real guest operations:
fallback bytes, and firmware BootOrder. Install Btrfs into `/dev/vda3` while fallback bytes, and firmware BootOrder. Install Btrfs into `/dev/vda3` while
retaining `/dev/vda1`, explicitly register a HydraOS entry, then verify those retaining `/dev/vda1`, explicitly register a HydraOS entry, then verify those
invariants before the harness deliberately sets BootNext to choose that entry. invariants before the harness deliberately sets BootNext to choose that entry.
Subsequent live boots also use verified one-shot BootNext selection. The same
SATA DVD drive stays attached throughout, empty during installed boots; no ISO
is available to those boots. This keeps OVMF's device entries stable without
resetting firmware variables or changing BootOrder. Every deliberate BootNext
write is read back and checked against the unchanged BootOrder. If OVMF prunes
HydraOS's create-only entry after consuming BootNext, the harness recreates only
that known ESP/loader entry before the next selection, and verifies that both
registration and selection preserve the original order. Live test consoles
reduce kernel console verbosity so asynchronous mount messages cannot corrupt
framed JSON; the messages remain in the guest kernel log.
2. Boot without ISO, verify the packaged Fastfetch art and doctor, install version 2. Boot without ISO, verify the packaged Fastfetch art and doctor, install version
1.0 of a locally created EAPI 8 Portage package, and create a real qcow2 plus 1.0 of a locally created EAPI 8 Portage package, and create a real qcow2 plus
profile/data sentinels under HOME. The fixture has no sources or dependencies; profile/data sentinels under HOME. The fixture has no sources or dependencies;
+24
View File
@@ -63,6 +63,30 @@ class SmokeTests(unittest.TestCase):
self.assertNotIn('media=cdrom', ' '.join(argv)) self.assertNotIn('media=cdrom', ' '.join(argv))
self.assertNotIn('-cdrom', argv) self.assertNotIn('-cdrom', argv)
def test_persistent_optical_drive_keeps_topology_with_no_installed_medium(self):
disk = self.root / 'disk.qcow2'
self.mod.reserve_disk(disk)
iso = self.root / 'trusted.iso'
iso.write_bytes(b'ISO')
optical = []
for medium in (iso, None):
argv = self.mod.qemu_argv('qemu', self.root, disk, self.code,
self.variables, medium, 2048, keep_cdrom_drive=True)
drives = [argv[i + 1] for i, value in enumerate(argv[:-1]) if value == '-drive']
cdrom = [dict(field.split('=', 1) for field in drive.split(','))
for drive in drives if 'media=cdrom' in drive]
self.assertEqual(len(cdrom), 1)
self.assertEqual(cdrom[0]['if'], 'ide')
self.assertEqual(cdrom[0]['index'], '0')
self.assertEqual(cdrom[0]['id'], 'acceptance-cdrom')
self.assertEqual(cdrom[0]['readonly'], 'on')
optical.append(cdrom[0])
self.assertEqual(optical[0].pop('file'), str(iso))
self.assertEqual(optical[0].pop('format'), 'raw')
self.assertNotIn('file', optical[1], 'Installed boot must have no ISO medium')
self.assertNotIn('format', optical[1], 'An empty drive must not request a raw block backend')
self.assertEqual(optical[0], optical[1])
def test_hardlinked_disk_is_refused(self): def test_hardlinked_disk_is_refused(self):
disk = self.root / 'disk.qcow2' disk = self.root / 'disk.qcow2'
self.mod.reserve_disk(disk) self.mod.reserve_disk(disk)
+84
View File
@@ -9,6 +9,38 @@ import urllib.error
import urllib.request import urllib.request
class BootConsole:
"""Captured efibootmgr format with observable guest NVRAM mutations."""
def __init__(self, entries=None, changed_order=False, drop_next=False, changed_create_order=False):
self.order = '0000,0001,0002,0003'
self.next = None
self.changed_order, self.drop_next = changed_order, drop_next
self.changed_create_order = changed_create_order
self.entries = entries if entries is not None else (
'Boot0000* UiApp\tFvVol(example)/FvFile(example)\n'
'Boot0001* UEFI QEMU DVD-ROM QM00001 \tPciRoot(0x0)/Pci(0x1f,0x2)/Sata(0,65535,0){auto_created_boot_option}\n'
'Boot0002* UEFI Misc Device\tPciRoot(0x0)/Pci(0x3,0x0)\n'
'Boot0003* EFI Internal Shell\tFvVol(example)/FvFile(example)\n'
'Boot0004* HydraOS\tHD(1,GPT,5d24ab30-9c80-4f5d-8594-f9f08fdf31fb,0x800,0x100000)/\\EFI\\HydraOS\\grubx64.efi\n')
def command(self, command, timeout):
if command == 'sudo -n blkid -s PARTUUID -o value /dev/vda1':
return b'5d24ab30-9c80-4f5d-8594-f9f08fdf31fb\n'
if command == "sudo -n efibootmgr --create-only --disk /dev/vda --part 1 --label HydraOS --loader '\\EFI\\HydraOS\\grubx64.efi'":
self.entries += ('Boot0004* HydraOS\tHD(1,GPT,5d24ab30-9c80-4f5d-8594-f9f08fdf31fb,0x800,0x100000)/\\EFI\\HydraOS\\grubx64.efi\n')
if self.changed_create_order:
self.order = '0004,0000,0001,0002,0003'
elif command.startswith('sudo -n efibootmgr --bootnext '):
if not self.drop_next:
self.next = command.split()[-1]
if self.changed_order:
self.order = '0001,0000,0002,0003'
elif command != 'sudo -n efibootmgr':
raise AssertionError('Unexpected firmware mutation: ' + command)
return ('BootCurrent: 0004\nTimeout: 1 seconds\nBootOrder: ' + self.order + '\n' +
('BootNext: ' + self.next + '\n' if self.next else '') + self.entries).encode()
SCRIPT = Path(__file__).resolve().parents[1] / 'dev/laptop-smoke.py' SCRIPT = Path(__file__).resolve().parents[1] / 'dev/laptop-smoke.py'
@@ -80,6 +112,58 @@ class LaptopSmokeTests(unittest.TestCase):
with self.assertRaises(self.mod.SmokeError): with self.assertRaises(self.mod.SmokeError):
self.mod.partition_installer_command('bad;token') self.mod.partition_installer_command('bad;token')
def test_live_boot_is_selected_once_without_changing_bootorder(self):
console = BootConsole()
self.mod.select_live(console)
self.assertEqual(console.next, '0001')
self.assertEqual(console.order, '0000,0001,0002,0003')
def test_live_boot_refuses_missing_ambiguous_or_inactive_dvd_entry(self):
for entries in ('Boot0001* Unrelated DVD\tPciRoot(0x0)\n',
'Boot0001* UEFI QEMU DVD-ROM QM00001 Other\tPciRoot(0x0)\n',
'Boot0001 UEFI QEMU DVD-ROM QM00001 \tPciRoot(0x0)\n',
'Boot0001* UEFI QEMU DVD-ROM QM00001 \tPciRoot(0x0)\n'
'Boot0002* UEFI QEMU DVD-ROM QM00001 \tPciRoot(0x0)\n'):
console = BootConsole(entries=entries)
with self.subTest(entries=entries), self.assertRaises(self.mod.SmokeError):
self.mod.select_live(console)
self.assertIsNone(console.next)
def test_boot_selection_verifies_effective_bootnext_and_unchanged_order(self):
for selector in ('select_live', 'select_installed'):
for fault in ('changed_order', 'drop_next'):
with self.subTest(selector=selector, fault=fault), self.assertRaises(self.mod.SmokeError):
getattr(self.mod, selector)(BootConsole(**{fault: True}))
def test_installed_boot_recreates_pruned_owned_entry_without_changing_order(self):
console = BootConsole()
console.entries = console.entries[:console.entries.index('Boot0004')]
with self.assertRaises(self.mod.SmokeError):
self.mod.select_installed(console)
self.assertIsNone(console.next, 'First selection must prove installer registration')
self.mod.select_installed(console, recreate=True)
self.assertEqual(console.next, '0004')
self.assertEqual(console.order, '0000,0001,0002,0003')
self.assertIn('\\EFI\\HydraOS\\grubx64.efi', console.entries)
def test_recreated_entry_cannot_change_bootorder(self):
console = BootConsole(changed_create_order=True)
console.entries = console.entries[:console.entries.index('Boot0004')]
with self.assertRaises(self.mod.SmokeError):
self.mod.select_installed(console, recreate=True)
self.assertIsNone(console.next)
def test_boot_selection_rejects_correct_label_with_foreign_device_or_loader(self):
for selector, before, after in (
('select_live', 'Sata(0,65535,0)', 'Sata(2,65535,0)'),
('select_installed', '5d24ab30-9c80-4f5d-8594-f9f08fdf31fb', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'),
('select_installed', '\\EFI\\HydraOS\\grubx64.efi', '\\EFI\\ForeignOS\\grubx64.efi')):
console = BootConsole()
console.entries = console.entries.replace(before, after)
with self.subTest(selector=selector, wrong=after), self.assertRaises(self.mod.SmokeError):
getattr(self.mod, selector)(console)
self.assertIsNone(console.next)
def test_base_state_compares_kernel_bytes_without_including_mounted_esp(self): def test_base_state_compares_kernel_bytes_without_including_mounted_esp(self):
# Offline /boot/efi is an empty mountpoint; on disk-only boot it contains # Offline /boot/efi is an empty mountpoint; on disk-only boot it contains
# the mounted ESP. That mount must not produce a false base mismatch. # the mounted ESP. That mount must not produce a false base mismatch.