Support safe installation into existing GPT partitions and Btrfs layouts

This commit is contained in:
Codex
2026-10-13 21:41:23 +03:00
parent ae0d9cf08d
commit 653388f0d7
6 changed files with 798 additions and 44 deletions
+319 -1
View File
@@ -43,6 +43,12 @@ class FakeProbe:
def physical_disk(self, path):
return path == '/dev/vda'
def verify_identity(self, path, node):
return None
def partition_number(self, path, node):
return {'/dev/vda1': 1, '/dev/vda2': 2, '/dev/vda3': 3}.get(path, 0)
class FakeRunner:
def __init__(self, root, fail=None):
@@ -68,11 +74,313 @@ class FakeRunner:
(self.root / 'etc/systemd/system/getty@tty1.service.d/90-hydra-live.conf').touch()
(self.root / 'etc/sudoers.d/90-hydra-live').touch()
(self.root / 'boot/vmlinuz-6.12.1').touch()
if argv[0] == 'unsquashfs' and '-lln' in argv:
return 'drwxr-xr-x 0/0 0 2026-10-13 12:00 squashfs-root\n-rw-r--r-- 0/0 2048 2026-10-13 12:00 squashfs-root/file\n'
if argv[0] == 'blkid':
return '11111111-2222-3333-4444-555555555555\n'
if 'PARTUUID' in argv:
number = '1' if argv[-1].endswith('1') else '2'
return 'aaaaaaaa-bbbb-cccc-dddd-' + number.zfill(12) + '\n'
return 'ABCD-1234\n' if argv[-1].endswith('1') else '11111111-2222-3333-4444-555555555555\n'
return ''
def partition_topology(*, sibling_mounted=False):
esp = disk('/dev/vda1', kind='part')
root = disk('/dev/vda2', kind='part')
sibling = disk('/dev/vda3', kind='part', mounts=['/other'] if sibling_mounted else [])
for node, start, size, number in ((esp, 2048, 1024**3, 1),
(root, 2099200, 8*1024**3, 2),
(sibling, 18876416, 4*1024**3, 3)):
node.update(start=start, size=size, partuuid=f'aaaaaaaa-bbbb-cccc-dddd-{number:012d}',
parttype='c12a7328-f81f-11d2-ba4b-00a0c93ec93b' if number == 1 else
'0fc63daf-8483-4772-8e79-3d69d8477de4',
fstype='vfat' if number == 1 else 'ext4', uuid='ABCD-1234' if number == 1 else
f'11111111-2222-3333-4444-{number:012d}')
return topology(disk(children=[esp, root, sibling]))
def partition_request(**values):
return i.Request(None, 'emil', root_partition='/dev/vda2', esp_partition='/dev/vda1',
**values)
class PartitionGuardTests(unittest.TestCase):
def test_explicit_partitions_allow_unrelated_mounted_sibling(self):
self.assertEqual(i.validate_request(partition_request(), FakeProbe(partition_topology(sibling_mounted=True))), '/dev/vda2')
def test_modes_and_confirmations_are_unambiguous(self):
for changes in ({'disk': '/dev/vda'}, {'esp_partition': None}, {'root_fs': 'xfs'},
{'confirm_erase': '/dev/vda'}, {'confirm_format': '/dev/vda3', 'execute': True}):
request = partition_request()
for key, value in changes.items():
setattr(request, key, value)
with self.subTest(changes=changes), self.assertRaises(i.InstallError):
i.validate_request(request, FakeProbe(partition_topology()))
self.assertEqual(i.validate_request(partition_request(execute=True, confirm_format='/dev/vda2'),
FakeProbe(partition_topology())), '/dev/vda2')
def test_selected_mount_holder_alias_overlap_or_bad_metadata_refused(self):
for mutation in ('mount', 'holder', 'same', 'bounds', 'overlap', 'missing', 'not-fat', 'not-esp', 'nested'):
topo = partition_topology()
req = partition_request()
if mutation == 'mount': topo.protect('/dev/vda2')
if mutation == 'holder':
topo.nodes['/dev/holder'] = disk('/dev/holder', kind='crypt')
topo.edges['/dev/holder'] = set(); topo.parents['/dev/holder'] = set()
topo.link('/dev/holder', '/dev/vda2')
if mutation == 'same': req.esp_partition = req.root_partition
if mutation == 'bounds': topo.nodes['/dev/vda2']['size'] = 40*1024**3
if mutation == 'overlap': topo.nodes['/dev/vda2']['start'] = 2048
if mutation == 'missing': del topo.nodes['/dev/vda2']['partuuid']
if mutation == 'not-fat': topo.nodes['/dev/vda1']['fstype'] = 'ext4'
if mutation == 'not-esp': topo.nodes['/dev/vda1']['parttype'] = '0fc63daf-8483-4772-8e79-3d69d8477de4'
if mutation == 'nested': topo.parents['/dev/vda2'].add('/dev/vda3')
with self.subTest(mutation=mutation), self.assertRaises(i.InstallError):
i.validate_request(req, FakeProbe(topo))
def test_malformed_optional_storage_metadata_is_rejected(self):
for field, value in (('partuuid', 123), ('uuid', []), ('parttype', {}), ('start', True)):
nodes = partition_topology().nodes
nodes['/dev/vda1'][field] = value
with self.subTest(field=field), self.assertRaises(i.InstallError):
topo = topology(nodes['/dev/vda'])
i.validate_request(partition_request(), FakeProbe(topo))
def test_btrfs_loop_backing_members_preserve_eligible_root(self):
nodes = list(partition_topology().nodes.values())[:1]
loop = disk('/dev/loop0', kind='loop', mounts=['/scratch']); loop['maj:min']='7:0'
media = disk('/dev/sr0', kind='rom', mounts=['/live']); media['maj:min']='11:0'
nodes.extend([loop,media])
btrfs={'source':'/dev/vda3[/home]', 'target':'/other', 'fstype':'btrfs','maj:min':'0:37','options':'rw'}
scratch={'source':'/dev/loop0','target':'/scratch','fstype':'ext4','maj:min':'7:0','options':'rw'}
live={'source':'/dev/sr0','target':'/live','fstype':'iso9660','maj:min':'11:0','options':'ro'}
class ReadOnlyRunner:
def run(self, argv):
if argv[0]=='lsblk': return json.dumps({'blockdevices':nodes})
if argv[0]=='btrfs':
return 'Label: none uuid: abc\n Total devices 1 FS bytes used 4096\n devid 1 size 999 used 1 path /dev/vda3\n'
return json.dumps({'filesystems':[live] if '--target' in argv else [btrfs,scratch,live]})
probe=i.SystemProbe(ReadOnlyRunner())
probe.physical_disk=lambda path: path=='/dev/vda'
probe.verify_identity=lambda path,node: None
probe.partition_number=lambda path,node: 1 if path.endswith('1') else 2
with patch.object(Path,'is_file',return_value=True), patch.object(i.SystemProbe,'_loop_backing_identity',return_value='0:37'):
topo=probe.snapshot(i.DEFAULT_PAYLOAD)
probe.snapshot=lambda payload:topo
self.assertIn('/dev/vda3',topo.parents['/dev/loop0'])
self.assertEqual(i.validate_request(partition_request(),probe),'/dev/vda2')
def test_partition_plan_formats_only_root_and_preserves_efi(self):
req = partition_request()
plan = i.render_plan(req, '/dev/vda2', '/mnt/target')
self.assertIn('mkfs.ext4', plan)
self.assertIn('--no-nvram', plan)
self.assertNotIn('--removable', plan)
self.assertNotIn('parted', plan)
self.assertNotIn('mkfs.vfat', plan)
self.assertIn('--confirm-format /dev/vda2', plan)
def test_btrfs_dry_run_includes_real_mount_layout(self):
plan=i.render_plan(partition_request(root_fs='btrfs'),'/dev/vda2','/mnt/target')
self.assertIn('subvol=@root',plan)
self.assertIn('subvol=@home',plan)
self.assertIn('subvol=@snapshots',plan)
self.assertLess(plan.index('ro,nosuid,nodev,noexec'),plan.index('mkfs.btrfs'))
def test_cli_accepts_partition_mode_without_disk(self):
output=io.StringIO()
with patch.object(i,'SystemProbe',return_value=FakeProbe(partition_topology())), contextlib.redirect_stdout(output):
self.assertEqual(i.main(['--root-partition','/dev/vda2','--esp-partition','/dev/vda1','--user','emil']),0)
self.assertIn('format ONLY /dev/vda2',output.getvalue())
def test_btrfs_mount_protects_all_members_without_protecting_siblings(self):
topo = partition_topology(sibling_mounted=True)
class ReadOnlyRunner:
def run(self, argv):
return ("Label: none uuid: 11111111-2222-3333-4444-555555555555\n"
"\tTotal devices 1 FS bytes used 4096\n"
"\tdevid 1 size 4294967296 used 4096 path /dev/vda3\n")
probe = i.SystemProbe(ReadOnlyRunner())
probe._block_source({'source':'/dev/vda3[/home]', 'target':'/other', 'fstype':'btrfs',
'maj:min':'0:37', 'options':'rw'}, topo)
self.assertIn('/dev/vda3', topo.protected)
self.assertEqual(i.validate_request(partition_request(), FakeProbe(topo)), '/dev/vda2')
def test_multi_member_btrfs_mount_protects_selected_target(self):
class ReadOnlyRunner:
def run(self, argv):
return ('Label: none uuid: abc\n Total devices 2 FS bytes used 4096\n'
' devid 1 size 999 used 1 path /dev/vda3\n'
' devid 2 size 999 used 1 path /dev/vda2\n')
topo = partition_topology()
i.SystemProbe(ReadOnlyRunner())._block_source(
{'source':'/dev/vda3', 'target':'/other', 'fstype':'btrfs', 'maj:min':'0:37'}, topo)
with self.assertRaises(i.InstallError):
i.validate_request(partition_request(), FakeProbe(topo))
def test_conflicting_duplicate_partition_metadata_is_rejected(self):
node = disk('/dev/vda1', kind='part', pkname='/dev/vda')
other = dict(node, start=9999)
with self.assertRaises(i.InstallError):
topology(disk(children=[dict(node, start=2048)]), other)
def test_btrfs_source_not_in_reported_members_is_rejected(self):
class ReadOnlyRunner:
def run(self, argv):
return 'Label: none uuid: abc\n Total devices 1 FS bytes used 4096\n devid 1 size 999 used 1 path /dev/vda3\n'
with self.assertRaises(i.InstallError):
i.SystemProbe(ReadOnlyRunner())._block_source(
{'source':'/dev/vda2', 'target':'/other', 'fstype':'btrfs', 'maj:min':'0:37'}, partition_topology())
def test_incomplete_btrfs_members_fail_closed(self):
class ReadOnlyRunner:
def run(self, argv):
return 'Label: none uuid: abc\n Total devices 2 FS bytes used 4096\n devid 1 size 999 used 1 path /dev/vda3\n'
with self.assertRaises(i.InstallError):
i.SystemProbe(ReadOnlyRunner())._block_source(
{'source':'/dev/vda3', 'target':'/other', 'fstype':'btrfs', 'maj:min':'0:37'}, partition_topology())
class PartitionPipelineTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(); self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)/'target'; self.root.mkdir()
self.probe = FakeProbe(partition_topology(sibling_mounted=True))
self.request = partition_request(execute=True, confirm_format='/dev/vda2')
def install(self, runner):
i.install(self.request, runner, self.probe, self.root, 'test-password', io.StringIO())
def test_existing_mode_formats_root_only_and_records_owned_layout(self):
runner = FakeRunner(self.root)
self.install(runner)
self.assertFalse(any(c[0] in ('parted', 'partprobe', 'mkfs.vfat') for c in runner.calls))
self.assertEqual([c[-1] for c in runner.calls if c[0].startswith('mkfs.')], ['/dev/vda2'])
self.assertFalse(any('--removable' in c for c in runner.calls))
self.assertFalse(any(c[0] == 'efibootmgr' for c in runner.calls))
ro = next(n for n,c in enumerate(runner.calls) if c[0]=='mount' and 'ro,nosuid,nodev,noexec' in c)
format_at = next(n for n,c in enumerate(runner.calls) if c[0]=='mkfs.ext4')
self.assertLess(ro, format_at)
marker = json.loads((self.root/'etc/hydra/storage.json').read_text())
self.assertEqual(marker['format'], 1); self.assertEqual(marker['filesystem'], 'ext4')
self.assertEqual(marker['owned_efi_path'], 'EFI/HydraOS')
self.assertEqual(stat.S_IMODE((self.root/'etc/hydra/storage.json').stat().st_mode), 0o600)
self.assertNotIn('root_subvolume', marker)
self.assertIn('GRUB_DISABLE_OS_PROBER=true', (self.root/'etc/default/grub').read_text())
def test_esp_collision_or_insufficient_space_prevents_formatting(self):
for problem in ('collision', 'space'):
runner = FakeRunner(self.root)
real_run = runner.run
def run(argv, **kw):
result = real_run(argv, **kw)
if argv[0]=='mount' and 'ro,nosuid,nodev,noexec' in argv and problem=='collision':
(Path(argv[-1])/'EFI/HydraOS').mkdir(parents=True)
return result
runner.run = run
context = patch('os.statvfs', return_value=SimpleNamespace(f_bavail=1, f_frsize=4096)) if problem=='space' else contextlib.nullcontext()
with self.subTest(problem=problem), context, self.assertRaises(i.InstallError):
self.install(runner)
self.assertFalse(any(c[0].startswith('mkfs.') for c in runner.calls))
def test_esp_preflight_failure_never_recursively_removes_foreign_files(self):
runner=FakeRunner(self.root); original_run=runner.run; sentinels=[]
def run(argv, **kw):
result=original_run(argv, **kw)
if argv[0]=='mount' and 'ro,nosuid,nodev,noexec' in argv:
sentinel=Path(argv[-1])/'EFI/HydraOS/foreign'
sentinel.parent.mkdir(parents=True); sentinel.write_bytes(b'keep')
sentinels.append(sentinel)
return result
runner.run=run
with self.assertRaises(i.InstallError): self.install(runner)
self.assertEqual(sentinels[0].read_bytes(), b'keep')
self.assertFalse(any(c[0].startswith('mkfs.') for c in runner.calls))
def test_identity_change_after_esp_preflight_prevents_formatting(self):
runner = FakeRunner(self.root)
original_snapshot = self.probe.snapshot
def snapshot(payload):
topo = original_snapshot(payload)
if self.probe.calls >= 2: topo.nodes['/dev/vda2']['maj:min'] = '252:9999'
return topo
self.probe.snapshot = snapshot
with self.assertRaises(i.InstallError): self.install(runner)
self.assertFalse(any(c[0].startswith('mkfs.') for c in runner.calls))
def test_btrfs_root_uses_named_subvolumes_and_boot_arguments(self):
self.request.root_fs = 'btrfs'
runner = FakeRunner(self.root); self.install(runner)
self.assertIn(['mkfs.btrfs', '-f', '-L', 'HYDRA_ROOT', '/dev/vda2'], runner.calls)
subvolumes = [Path(c[-1]).name for c in runner.calls if c[:3]==['btrfs','subvolume','create']]
self.assertEqual(subvolumes, ['@root','@home','@snapshots'])
fstab=(self.root/'etc/fstab').read_text()
for name in ('@root','@home','@snapshots'): self.assertIn('subvol='+name, fstab)
self.assertNotIn('subvolid', fstab)
self.assertIn('rootflags=subvol=@root', (self.root/'etc/default/grub').read_text())
self.assertIn('rootflags=subvol=@root', (self.root/'etc/dracut.conf.d/90-hydra-installed.conf').read_text())
self.assertEqual(json.loads((self.root/'etc/hydra/storage.json').read_text())['root_subvolume'],'@root')
self.assertEqual(runner.calls[-1], ['umount', str(self.root)])
def test_foreign_efi_files_are_compared_after_loader_install(self):
runner=FakeRunner(self.root)
original_run=runner.run
def run(argv, **kw):
result=original_run(argv, **kw)
if argv[0]=='mount' and argv[-2]=='/dev/vda1':
foreign=Path(argv[-1])/'EFI/BOOT/BOOTX64.EFI'
foreign.parent.mkdir(parents=True, exist_ok=True); foreign.write_bytes(b'foreign-loader')
if 'grub-install' in argv:
(self.root/'boot/efi/EFI/BOOT/BOOTX64.EFI').write_bytes(b'changed-loader')
return result
runner.run=run
with self.assertRaisesRegex(i.InstallError, 'Foreign ESP files changed'):
self.install(runner)
def test_payload_capacity_failure_occurs_before_root_format(self):
runner=FakeRunner(self.root); original_run=runner.run
def run(argv, **kw):
if '-lln' in argv:
return '-rw-r--r-- 0/0 10737418240 2026-10-13 12:00 squashfs-root/large\n'
return original_run(argv, **kw)
runner.run=run
with self.assertRaisesRegex(i.InstallError, 'capacity'):
self.install(runner)
self.assertFalse(any(c[0].startswith('mkfs.') for c in runner.calls))
def test_btrfs_extraction_failure_cleans_home_snapshots_and_root(self):
self.request.root_fs='btrfs'; runner=FakeRunner(self.root,'unsquashfs-extract')
with self.assertRaises(i.InstallError): self.install(runner)
self.assertEqual(runner.calls[-3:], [['umount',str(self.root/'.snapshots')],
['umount',str(self.root/'home')], ['umount',str(self.root)]])
def test_registration_creates_only_new_entry(self):
self.request.register_boot_entry=True
runner=FakeRunner(self.root); self.install(runner)
commands=[c for c in runner.calls if c[0]=='efibootmgr']
self.assertEqual(len(commands),1)
self.assertIn('--create-only', commands[0]); self.assertNotIn('--bootorder',commands[0])
self.assertIn('/dev/vda',commands[0]); self.assertIn('1',commands[0])
class PayloadCapacityTests(unittest.TestCase):
def test_capacity_reserves_blocks_for_many_small_inodes(self):
listing='-rw-r--r-- 0/0 1 2026-10-13 12:00 squashfs-root/tiny\n' * 500000
self.assertGreater(i.storage.payload_capacity(listing), 4*1024**3)
def test_payload_special_inodes_do_not_need_device_capacity(self):
listing=('drwxr-xr-x 0/0 39 2026-10-13 12:00 squashfs-root\n'
'crw-rw-rw- 0/0 1, 3 2026-10-13 12:00 squashfs-root/dev/null\n'
'brw-rw---- 0/0 8, 0 2026-10-13 12:00 squashfs-root/dev/disk\n')
self.assertEqual(i.storage.payload_capacity(listing),4*1024**3)
def test_unrecognized_payload_listing_fails_closed(self):
for listing in ('', '-rw-r--r-- root/root unknown file', 'device entry cannot be measured'):
with self.subTest(listing=listing), self.assertRaises(i.InstallError):
i.storage.payload_capacity(listing)
class GuardTests(unittest.TestCase):
def setUp(self):
self.assertIsNotNone(i, 'installer module must implement the guarded installer')
@@ -182,6 +490,14 @@ class PipelineTests(unittest.TestCase):
self.assertIn('6.12.1', dracut)
self.assertEqual(runner.calls[-1], ['umount', str(self.root)])
def test_whole_disk_installs_owned_loader_and_fallback(self):
runner=FakeRunner(self.root); self.install(runner)
boot_commands=[c for c in runner.calls if 'grub-install' in c]
self.assertEqual(len(boot_commands),2)
self.assertNotIn('--removable',boot_commands[0])
self.assertIn('--removable',boot_commands[1])
self.assertEqual(json.loads((self.root/'etc/hydra/storage.json').read_text())['owned_efi_path'], 'EFI/HydraOS')
def test_partition_format_and_extract_failures_abort_and_cleanup_only_mounted(self):
for failing in ('parted', 'mkfs.vfat', 'mkfs.ext4', 'unsquashfs-extract'):
with self.subTest(failing=failing):
@@ -452,6 +768,7 @@ class RelocatedLiveLoopTests(unittest.TestCase):
self.assertEqual(syscall.call_args.args, (91, 0x4C05, bytes(232)))
self.assertFalse(any('/LiveOS/squashfs.img' in args for args in calls))
probe.snapshot = lambda payload: topo
probe.verify_identity = lambda path, node: None
self.assertEqual(i.validate_request(i.Request('/dev/vda', 'hydratest'), probe), '/dev/vda')
probe.physical_disk = lambda path: path == '/dev/sr0'
with self.assertRaises(i.InstallError):
@@ -522,6 +839,7 @@ class NestedLoopAncestryTests(unittest.TestCase):
side_effect=lambda path, expected: identities[path]):
topo = probe.snapshot(i.DEFAULT_PAYLOAD)
probe.snapshot = lambda payload: topo
probe.verify_identity = lambda path, node: None
return probe, topo
def test_reverse_order_nested_loop_refuses_physical_backing(self):