Files
Hydra-OS/tests/test_installer.py
T

1035 lines
55 KiB
Python

"""Installer tests never invoke a real partition, mount or bootloader command."""
import contextlib
import io
import json
import os
import pty
import stat
import struct
from types import SimpleNamespace
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
try:
from hydra import installer as i
except ImportError:
i = None
def disk(path='/dev/vda', children=None, mounts=None, kind='disk', pkname=None):
return {'path': path, 'type': kind, 'pkname': pkname, 'mountpoints': [] if mounts is None else mounts,
'size': 32 * 1024 ** 3, 'maj:min': '252:' + str(sum((n + 1) * ord(c) for n, c in enumerate(path))), 'children': children or []}
def topology(*nodes):
return i.Topology.from_json(json.dumps({'blockdevices': list(nodes)}))
class FakeProbe:
def __init__(self, topo=None):
self.topo = topo or topology(disk())
self.calls = 0
def snapshot(self, payload):
self.calls += 1
return self.topo
def resolve(self, path):
return {'/dev/disk/by-id/test': '/dev/vda'}.get(path, path)
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):
self.root, self.fail, self.calls, self.inputs = root, fail, [], []
def run(self, argv, *, input=None, sensitive=False):
self.calls.append(list(argv))
self.inputs.append((input, sensitive))
if self.fail == argv[0] or (argv[0] == 'chroot' and self.fail == argv[2]) or (self.fail == 'unsquashfs-extract' and argv[0] == 'unsquashfs' and '-d' in argv):
raise i.InstallError('command failed: ' + argv[0])
if argv[0] == 'unsquashfs' and '-d' in argv:
for directory in ('etc/hydra/live-mode', 'etc/systemd/system/getty@tty1.service.d',
'etc/sudoers.d', 'usr/share/zoneinfo/Europe', 'etc/locale.gen', 'lib/modules/6.12.1', 'boot'):
if directory != 'etc/locale.gen':
(self.root / directory).mkdir(parents=True, exist_ok=True)
(self.root / 'etc/passwd').write_text('root:x:0:0:root:/root:/bin/bash\n')
(self.root / 'usr/share/zoneinfo/Europe/Moscow').touch()
(self.root / 'etc/locale.gen').write_text('# en_US.UTF-8 UTF-8\n')
(self.root / 'etc/hydra/live-mode/active').touch()
(self.root / 'etc/hydra/live-mode/remove-on-install.json').write_text(json.dumps([
'/etc/systemd/system/getty@tty1.service.d/90-hydra-live.conf',
'/etc/sudoers.d/90-hydra-live']))
(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':
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')
def request(self, **kw):
return i.Request(**({'disk': '/dev/vda', 'user': 'emil'} | kw))
def test_confirmation_missing_and_mismatched(self):
for confirmation in (None, '/dev/vdb', '/dev/disk/by-id/test'):
with self.subTest(confirmation=confirmation), self.assertRaises(i.InstallError):
i.validate_request(self.request(execute=True, confirm_erase=confirmation), FakeProbe())
self.assertEqual(i.validate_request(self.request(execute=True, confirm_erase='/dev/vda'), FakeProbe()), '/dev/vda')
def test_alias_resolved_but_confirmation_must_name_resolved_target(self):
self.assertEqual(i.validate_request(self.request(disk='/dev/disk/by-id/test', execute=True,
confirm_erase='/dev/vda'), FakeProbe()), '/dev/vda')
def test_disk_mounted_child_root_lvm_and_live_source_rejected(self):
examples = [
topology(disk(mounts=['/mnt'])),
topology(disk(children=[disk('/dev/vda1', kind='part', mounts=['/'])])),
topology(disk(children=[disk('/dev/vda2', kind='part', children=[disk('/dev/mapper/root', kind='lvm', mounts=['/'])])])),
]
live = topology(disk(children=[disk('/dev/vda1', kind='part')]))
live.protect('/dev/vda1')
examples.append(live)
for topo in examples:
with self.subTest(topo=topo), self.assertRaises(i.InstallError):
i.validate_request(self.request(), FakeProbe(topo))
def test_loop_backing_mount_protects_parent_disk(self):
topo = topology(disk(children=[disk('/dev/vda1', kind='part')]), disk('/dev/loop0', kind='loop', mounts=['/run/live']))
topo.link('/dev/loop0', '/dev/vda1')
with self.assertRaises(i.InstallError):
i.validate_request(self.request(), FakeProbe(topo))
def test_malformed_topology_fails_closed(self):
bad = ['{}', '{', '{"blockdevices":null}', '{"blockdevices":[]}',
json.dumps({'blockdevices': [disk(kind='mystery')]}),
json.dumps({'blockdevices': [disk(mounts='')] }),
json.dumps({'blockdevices': [disk(pkname='/dev/missing')]}),
json.dumps({'blockdevices': [disk(children=[disk('/dev/vda', kind='part')])]}),
json.dumps({'blockdevices': [disk('/dev/vda;reboot')]}),
json.dumps({'blockdevices': [disk(children='bad')]} )]
for data in bad:
with self.subTest(data=data), self.assertRaises(i.InstallError):
i.Topology.from_json(data)
def test_nonphysical_unknown_or_partition_target_rejected(self):
for path in ('/dev/vdb', '/tmp/fake', '/dev/vda1', '/dev/loop0', '/dev/vda;touch /tmp/x'):
with self.subTest(path=path), self.assertRaises(i.InstallError):
i.validate_request(self.request(disk=path), FakeProbe())
def test_username_timezone_and_payload_injection_rejected(self):
for username in ('root', '-u', 'a;reboot', 'a\nb', '../x', 'A', ''):
with self.subTest(username=username), self.assertRaises(i.InstallError):
i.validate_request(self.request(user=username), FakeProbe())
for timezone in ('../../etc/passwd', '/etc/passwd', 'a;reboot'):
with self.subTest(timezone=timezone), self.assertRaises(i.InstallError):
i.validate_request(self.request(timezone=timezone), FakeProbe())
for payload in ('-evil', '/tmp/a\nb'):
with self.subTest(payload=payload), self.assertRaises(i.InstallError):
i.validate_request(self.request(payload=payload), FakeProbe())
def test_plan_is_nonsecret_and_has_exact_partition_boot_commands(self):
plan = i.render_plan(self.request(), '/dev/vda', '/mnt/hydra-target')
self.assertIn('1MiB', plan)
self.assertIn('1025MiB', plan)
self.assertIn('--removable --no-nvram', plan)
self.assertIn('/run/initramfs/live/LiveOS/squashfs.img', plan)
self.assertIn('dry-run', plan)
self.assertNotIn('chpasswd:', plan)
class PipelineTests(unittest.TestCase):
def setUp(self):
self.assertIsNotNone(i, 'installer module must implement the guarded installer')
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name) / 'target'
self.root.mkdir()
self.probe = FakeProbe()
self.log = io.StringIO()
self.request = i.Request('/dev/vda', 'emil', execute=True, confirm_erase='/dev/vda')
def install(self, runner, password='distinctive-private-password'):
i.install(self.request, runner, self.probe, self.root, password, self.log)
def test_full_fake_pipeline_sets_installed_defaults_and_cleans_live_files(self):
runner = FakeRunner(self.root)
self.install(runner)
self.assertIn('UUID=', (self.root / 'etc/fstab').read_text())
self.assertIn('ru_RU.UTF-8 UTF-8', (self.root / 'etc/locale.gen').read_text())
self.assertFalse((self.root / 'etc/hydra/live-mode').exists())
self.assertFalse((self.root / 'etc/sudoers.d/90-hydra-live').exists())
self.assertIn(['chroot', str(self.root), 'grub-install', '--target=x86_64-efi', '--efi-directory=/boot/efi', '--bootloader-id=HydraOS', '--removable', '--no-nvram'], runner.calls)
password_commands = [(args, data) for args, data in zip(runner.calls, runner.inputs) if 'chpasswd' in args]
self.assertEqual(len(password_commands), 1)
self.assertEqual(password_commands[0][1], ('emil:distinctive-private-password\n', True))
self.assertNotIn('distinctive-private-password', self.log.getvalue())
self.assertGreaterEqual(self.probe.calls, 2)
self.assertIn('hostonly_cmdline="no"', (self.root / 'etc/dracut.conf.d/90-hydra-installed.conf').read_text())
self.assertIn('omit_dracutmodules+=" dmsquash-live "', (self.root / 'etc/dracut.conf.d/90-hydra-installed.conf').read_text())
self.assertNotIn('root=live:', (self.root / 'etc/default/grub').read_text())
self.assertIn('GRUB_DISTRIBUTOR="Hydra OS"', (self.root / 'etc/default/grub').read_text())
dracut = next(c for c in runner.calls if 'dracut' in c)
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):
runner = FakeRunner(self.root, failing)
with self.assertRaises(i.InstallError):
self.install(runner)
self.assertFalse(any('grub-install' in c for c in runner.calls))
index = next(n for n, c in enumerate(runner.calls) if c[0] == failing or (failing == 'unsquashfs-extract' and c[0] == 'unsquashfs' and '-d' in c))
self.assertTrue(all(c[0] == 'umount' for c in runner.calls[index + 1:]))
if failing == 'unsquashfs-extract':
self.assertEqual(runner.calls[-1], ['umount', str(self.root)])
self.assertIn(failing.split('-extract')[0], self.log.getvalue())
def test_unmount_failure_is_visible(self):
runner = FakeRunner(self.root, 'umount')
with self.assertRaises(i.InstallError):
self.install(runner)
self.assertIn('cleanup', self.log.getvalue())
def test_password_newlines_rejected_before_commands(self):
for password in ('x\ny', 'x\ry', '', 'x\x00y'):
runner = FakeRunner(self.root)
with self.subTest(password=password), self.assertRaises(i.InstallError):
self.install(runner, password)
self.assertEqual(runner.calls, [])
def test_dry_run_cannot_enter_install_pipeline(self):
runner = FakeRunner(self.root)
self.request.execute = False
with self.assertRaises(i.InstallError):
self.install(runner)
self.assertEqual(runner.calls, [])
def test_live_manifest_arbitrary_paths_and_symlink_parents_refused(self):
for path in ('/etc/passwd', '/home/emil/data', '/../etc/sudoers.d/90-hydra-live'):
directory = self.root / 'etc/hydra/live-mode'
directory.mkdir(parents=True, exist_ok=True)
(directory / 'active').touch()
(directory / 'remove-on-install.json').write_text(json.dumps([path]))
with self.subTest(path=path), self.assertRaises(i.InstallError):
i.remove_live_files(self.root)
outside = Path(self.tmp.name) / 'outside'
outside.mkdir()
(outside / '90-hydra-live').write_text('must remain')
(self.root / 'etc/sudoers.d').symlink_to(outside)
(directory / 'remove-on-install.json').write_text(json.dumps(['/etc/sudoers.d/90-hydra-live']))
with self.assertRaises(i.InstallError):
i.remove_live_files(self.root)
self.assertTrue((outside / '90-hydra-live').exists())
def test_missing_or_multiple_kernels_are_explicit_errors(self):
with self.assertRaises(i.InstallError):
i.select_kernel(self.root, None)
for version in ('6.12.1', '6.12.2'):
(self.root / 'lib/modules' / version).mkdir(parents=True)
(self.root / 'boot').mkdir(exist_ok=True)
(self.root / 'boot' / ('vmlinuz-' + version)).touch()
with self.assertRaises(i.InstallError):
i.select_kernel(self.root, None)
self.assertEqual(i.select_kernel(self.root, '6.12.2'), '6.12.2')
class RunnerTests(unittest.TestCase):
def setUp(self):
self.assertIsNotNone(i, 'installer module must implement the guarded installer')
def test_sensitive_failure_never_includes_stderr_or_stdin(self):
with patch('subprocess.run', return_value=subprocess.CompletedProcess([], 1, '', 'private-password')):
with self.assertRaises(i.InstallError) as caught:
i.Runner().run(['chroot', '/target', 'chpasswd'], input='user:private-password\n', sensitive=True)
self.assertNotIn('private-password', str(caught.exception))
def test_subprocess_uses_argv_without_shell_and_checked_returncode(self):
with patch('subprocess.run', return_value=subprocess.CompletedProcess([], 0, 'ok', '')) as call:
self.assertEqual(i.Runner().run(['tool', '/tmp/path;echo bad']), 'ok')
self.assertEqual(call.call_args.args[0], ['tool', '/tmp/path;echo bad'])
self.assertFalse(call.call_args.kwargs.get('shell', False))
class ProbeTests(unittest.TestCase):
def setUp(self):
self.assertIsNotNone(i, 'installer module required')
def test_device_mapper_alias_and_dev_root_resolve_by_identity(self):
topo = topology(disk('/dev/vda', children=[disk('/dev/dm-0', kind='lvm')]))
mount = {'source': '/dev/root', 'target': '/', 'fstype': 'ext4', 'maj:min': topo.nodes['/dev/vda']['maj:min'], 'options': 'rw'}
probe = i.SystemProbe()
self.assertEqual(probe._block_source(mount, topo), '/dev/vda')
self.assertIn('/dev/vda', topo.protected)
def test_parent_reference_cycles_rejected(self):
data = json.dumps({'blockdevices': [disk('/dev/a', pkname='/dev/b'), disk('/dev/b', pkname='/dev/a')]})
with self.assertRaises(i.InstallError):
i.Topology.from_json(data)
def test_malformed_findmnt_rejected(self):
for data in ('{}', '{', '{"filesystems":[]}', '{"filesystems":[{"source":"overlay"}]}'):
with self.subTest(data=data), self.assertRaises(i.InstallError):
i.SystemProbe._mounts(data)
def test_unknown_mounted_block_source_refused(self):
mount = {'source': '/dev/missing', 'target': '/', 'fstype': 'ext4', 'maj:min': '100:1', 'options': 'rw'}
with self.assertRaises(i.InstallError):
i.SystemProbe()._block_source(mount, topology(disk()))
def test_payload_symlink_never_resolves_into_host_from_target(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / 'usr/lib/modules/6.12.1').mkdir(parents=True)
(root / 'lib').symlink_to('/usr/lib')
(root / 'boot').mkdir()
(root / 'boot/vmlinuz-6.12.1').touch()
self.assertEqual(i.select_kernel(root, None), '6.12.1')
with self.assertRaises(i.InstallError):
i.target_path(root, '/lib/modules')
def test_leaf_symlink_removal_does_not_follow_link(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) / 'root'
outside = Path(tmp) / 'keep'
outside.write_text('keep')
(root / 'etc/hydra/live-mode').mkdir(parents=True)
(root / 'etc/sudoers.d').mkdir()
(root / 'etc/hydra/live-mode/active').touch()
(root / 'etc/hydra/live-mode/remove-on-install.json').write_text(json.dumps(['/etc/sudoers.d/90-hydra-live']))
(root / 'etc/sudoers.d/90-hydra-live').symlink_to(outside)
i.remove_live_files(root)
self.assertEqual(outside.read_text(), 'keep')
self.assertFalse((root / 'etc/sudoers.d/90-hydra-live').is_symlink())
def test_read_only_cli_invalid_target_and_confirmation_never_prompt_password(self):
with patch.object(i, 'getpass') as password, contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(i.main(['--disk', '/dev/hydra-this-device-does-not-exist', '--user', 'emil']), 1)
self.assertEqual(i.main(['--execute']), 1)
password.getpass.assert_not_called()
def test_preflight_refuses_container_before_execution(self):
with patch('os.geteuid', return_value=0), patch.object(i.sys.stdin, 'isatty', return_value=True), \
patch.object(i.Path, 'exists', return_value=True):
with self.assertRaisesRegex(i.InstallError, 'container'):
i.preflight(i.Request('/dev/vda', 'emil'), FakeRunner(Path('/unused')))
class AccountTests(unittest.TestCase):
def setUp(self):
self.assertIsNotNone(i)
def test_live_account_removed_without_touching_other_home(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / 'etc').mkdir()
(root / 'etc/passwd').write_text('root:x:0:0:root:/root:/bin/bash\nhydra-live:x:1000:1000:Live:/home/hydra-live:/bin/bash\n')
(root / 'home/hydra-live').mkdir(parents=True)
(root / 'home/hydra-live/config').touch()
(root / 'home/keep').mkdir()
runner = FakeRunner(root)
i.remove_live_account(root, runner.run)
self.assertIn(['chroot', str(root), 'userdel', 'hydra-live'], runner.calls)
self.assertFalse((root / 'home/hydra-live').exists())
self.assertTrue((root / 'home/keep').exists())
def test_live_account_unexpected_home_or_root_uid_refused(self):
for row in ('hydra-live:x:1000:1000:Live:/home/other:/bin/bash',
'hydra-live:x:0:0:Live:/home/hydra-live:/bin/bash'):
with self.subTest(row=row), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / 'etc').mkdir()
(root / 'etc/passwd').write_text(row + '\n')
runner = FakeRunner(root)
with self.assertRaises(i.InstallError):
i.remove_live_account(root, runner.run)
self.assertEqual(runner.calls, [])
def test_live_manifest_nested_unit_paths_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
live = root / 'etc/hydra/live-mode'
live.mkdir(parents=True)
(live / 'active').touch()
(live / 'remove-on-install.json').write_text(json.dumps(['/etc/systemd/system/getty@foo/bar.service.d/90-hydra-live.conf']))
with self.assertRaises(i.InstallError):
i.remove_live_files(root)
class AdditionalFailureTests(unittest.TestCase):
setUp = PipelineTests.setUp
install = PipelineTests.install
def test_later_chroot_failure_aborts_bootloader_and_unmounts_all(self):
runner = FakeRunner(self.root, 'dracut')
with self.assertRaises(i.InstallError):
self.install(runner)
self.assertFalse(any('grub-install' in c for c in runner.calls))
self.assertEqual([c for c in runner.calls if c[0] == 'umount'], [
['umount', '--recursive', str(self.root / 'sys')],
['umount', str(self.root / 'proc')],
['umount', '--recursive', str(self.root / 'dev')],
['umount', str(self.root / 'boot/efi')],
['umount', str(self.root)],
])
def test_revalidation_detects_mounted_disk_before_partitioning(self):
class ChangedProbe(FakeProbe):
def snapshot(self, payload):
self.calls += 1
return topology(disk(mounts=['/mounted'])) if self.calls > 1 else self.topo
self.probe = ChangedProbe()
runner = FakeRunner(self.root)
with self.assertRaises(i.InstallError):
self.install(runner)
self.assertFalse(any(c[0] == 'parted' for c in runner.calls))
def test_unknown_multidevice_filesystem_refused(self):
topo = topology(disk())
for kind in ('zfs', 'btrfs'):
mount = {'source': 'pool' if kind == 'zfs' else '/dev/vda',
'target': '/', 'fstype': kind, 'maj:min': '0:22', 'options': 'rw'}
with self.subTest(kind=kind), self.assertRaises(i.InstallError):
i.SystemProbe()._block_source(mount, topo)
def test_prompt_requires_controlling_tty(self):
with patch('builtins.open', side_effect=OSError('no tty')):
with self.assertRaises(i.InstallError):
i.prompt_password()
class RelocatedLiveLoopTests(unittest.TestCase):
def probe(self, *, backing_device=None, backing_rdevice=0, ioctl_error=None,
opened_device=None, opened_mode=stat.S_IFBLK):
fixtures = Path(__file__).parent / 'fixtures'
data = json.loads((fixtures / 'installer-dracut-lsblk.json').read_text())
mounts = json.loads((fixtures / 'installer-dracut-findmnt.json').read_text())
media = next(m for m in i.SystemProbe._mounts(json.dumps(mounts)) if m['source'] == '/dev/sr0')
calls = []
class ReadOnlyRunner:
def run(self, args):
calls.append(args)
if args[0] == 'lsblk':
return json.dumps(data)
if '--target' not in args:
return json.dumps(mounts)
if args[args.index('--target') + 1] != i.DEFAULT_PAYLOAD:
raise i.InstallError('Real dracut stale backing path has no current mount')
return json.dumps({'filesystems': [media]})
probe = i.SystemProbe(ReadOnlyRunner())
probe.physical_disk = lambda path: path == '/dev/vda'
packet = struct.pack('=QQQ', os.makedev(11, 0) if backing_device is None else backing_device,
3462, backing_rdevice) + bytes(208)
with contextlib.ExitStack() as stack:
stack.enter_context(patch.object(Path, 'is_file', return_value=True))
stack.enter_context(patch.object(Path, 'read_text', return_value='/LiveOS/squashfs.img'))
opened = stack.enter_context(patch('os.open', return_value=91))
closed = stack.enter_context(patch('os.close'))
stack.enter_context(patch('os.fstat', return_value=SimpleNamespace(st_mode=opened_mode,
st_rdev=os.makedev(7, 0) if opened_device is None else opened_device)))
# Patch the syscall even before the installer imports fcntl.
syscall = stack.enter_context(patch('fcntl.ioctl', side_effect=ioctl_error,
return_value=packet))
topo = probe.snapshot(i.DEFAULT_PAYLOAD)
opened.assert_called_once_with('/dev/loop0', os.O_RDONLY | os.O_CLOEXEC)
closed.assert_called_once_with(91)
return probe, topo, calls, syscall
def test_actual_switched_root_topology_resolves_loop_by_kernel_identity(self):
probe, topo, calls, syscall = self.probe()
self.assertFalse(topo.component('/dev/vda') & topo.protected)
self.assertIn('/dev/loop0', topo.component('/dev/sr0'))
self.assertTrue(topo.component('/dev/sr0') & topo.protected)
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):
i.validate_request(i.Request('/dev/sr0', 'hydratest'), probe)
# A mounted loop backed by a normal disk still forbids that whole disk.
topo.nodes['/dev/sr0']['type'] = 'disk'
topo.nodes['/dev/sr0']['size'] = 32 * 1024 ** 3
with self.assertRaisesRegex(i.InstallError, 'backs mounted/root/live storage'):
i.validate_request(i.Request('/dev/sr0', 'hydratest'), probe)
def test_unknown_loop_filesystem_identity_fails_closed(self):
for identity in (os.makedev(8, 1), os.makedev(0, 99)):
with self.subTest(identity=identity), self.assertRaisesRegex(i.InstallError, 'Unknown physical backing'):
self.probe(backing_device=identity)
def test_unreadable_or_changed_loop_device_fails_closed(self):
cases = [{'ioctl_error': OSError('cannot read loop status')},
{'opened_device': os.makedev(7, 1)}, {'opened_mode': stat.S_IFREG}]
for case in cases:
with self.subTest(case=case), self.assertRaises(i.InstallError):
self.probe(**case)
def test_truncated_loop_status_fails_closed_and_closes_descriptor(self):
with patch('os.open', return_value=91), patch('os.close') as closed, \
patch('os.fstat', return_value=SimpleNamespace(st_mode=stat.S_IFBLK, st_rdev=os.makedev(7, 0))), \
patch('fcntl.ioctl', return_value=bytes(16)):
with self.assertRaisesRegex(i.InstallError, 'Malformed loop status'):
i.SystemProbe._loop_backing_identity('/dev/loop0', '7:0')
closed.assert_called_once_with(91)
def test_direct_block_backing_uses_kernel_rdevice(self):
_, topo, _, _ = self.probe(backing_device=os.makedev(0, 6), backing_rdevice=os.makedev(11, 0))
self.assertIn('/dev/loop0', topo.component('/dev/sr0'))
class NestedLoopAncestryTests(unittest.TestCase):
def probe(self, backings=None, *, indirect=False, payload_loop=False, shared=False):
loop0 = disk('/dev/loop0', kind='loop', mounts=['/mnt/also'] if shared else [])
loop0['maj:min'] = '7:0'
loop1 = disk('/dev/loop1', kind='loop', mounts=[] if payload_loop else ['/mnt/valuable'])
loop1['maj:min'] = '7:1'
target, unrelated = disk('/dev/vda'), disk('/dev/vdb')
target['maj:min'], unrelated['maj:min'] = '253:0', '253:1'
media = disk('/dev/sr0', kind='rom', mounts=['/run/initramfs/live'])
media['maj:min'] = '11:0'
nodes = [loop0, loop1, target, unrelated, media]
if indirect:
partition = disk('/dev/loop0p1', kind='part', pkname='/dev/loop0')
partition['maj:min'] = '259:0'
nodes.insert(1, partition)
identities = backings or {'/dev/loop0': '253:0',
'/dev/loop1': '253:0' if shared else '259:0' if indirect else '7:0'}
mount = {'source': '/dev/loop1' if payload_loop else '/dev/sr0',
'target': '/payload' if payload_loop else '/run/initramfs/live',
'fstype': 'ext4' if payload_loop else 'iso9660',
'maj:min': '7:1' if payload_loop else '11:0', 'options': 'ro'}
class ReadOnlyRunner:
def run(self, args):
if args[0] == 'lsblk':
return json.dumps({'blockdevices': nodes})
full_mount = {'source': '/dev/sr0', 'target': '/run/initramfs/live',
'fstype': 'iso9660', 'maj:min': '11:0', 'options': 'ro'}
return json.dumps({'filesystems': [mount] if '--target' in args else [full_mount]})
probe = i.SystemProbe(ReadOnlyRunner())
probe.physical_disk = lambda path: path in {'/dev/vda', '/dev/vdb'}
with patch.object(Path, 'is_file', return_value=True), \
patch.object(i.SystemProbe, '_loop_backing_identity',
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):
probe, _ = self.probe()
with self.assertRaisesRegex(i.InstallError, 'backs mounted/root/live storage'):
i.validate_request(i.Request('/dev/vda', 'hydratest'), probe)
def test_reverse_order_nested_loop_allows_unrelated_disk(self):
probe, topo = self.probe()
self.assertIn('/dev/vda', topo.component('/dev/loop1'))
self.assertEqual(i.validate_request(i.Request('/dev/vdb', 'hydratest'), probe), '/dev/vdb')
def test_unknown_nested_backing_refuses_unrelated_installation(self):
with self.assertRaisesRegex(i.InstallError, 'Unknown physical backing'):
self.probe({'/dev/loop1': '7:0', '/dev/loop0': '0:99'})
def test_nested_backing_cycles_fail_closed(self):
for backings in ({'/dev/loop1': '7:0', '/dev/loop0': '7:1'},
{'/dev/loop1': '7:1', '/dev/loop0': '253:0'}):
with self.subTest(backings=backings), self.assertRaisesRegex(i.InstallError, '[Cc]yclic'):
self.probe(backings)
def test_nested_backing_through_loop_partition_refuses_disk(self):
probe, _ = self.probe(indirect=True)
with self.assertRaisesRegex(i.InstallError, 'backs mounted/root/live storage'):
i.validate_request(i.Request('/dev/vda', 'hydratest'), probe)
def test_cycle_through_loop_partition_fails_closed(self):
with self.assertRaisesRegex(i.InstallError, '[Cc]yclic'):
self.probe({'/dev/loop1': '259:0', '/dev/loop0': '7:1'}, indirect=True)
def test_unmounted_payload_loop_ancestors_refuse_disk(self):
probe, _ = self.probe(payload_loop=True)
with self.assertRaisesRegex(i.InstallError, 'backs mounted/root/live storage'):
i.validate_request(i.Request('/dev/vda', 'hydratest'), probe)
def test_two_loops_on_same_backing_are_not_a_directed_cycle(self):
probe, topo = self.probe(shared=True)
self.assertIn('/dev/loop0', topo.component('/dev/loop1'))
self.assertEqual(i.validate_request(i.Request('/dev/vdb', 'hydratest'), probe), '/dev/vdb')
class SnapshotTests(unittest.TestCase):
def test_snapshot_connects_live_loop_to_its_physical_backing(self):
with tempfile.TemporaryDirectory() as tmp:
payload = Path(tmp) / 'squashfs.img'
payload.touch()
data = {'blockdevices': [disk(), disk('/dev/vdb', children=[disk('/dev/vdb1', kind='part', mounts=['/run/initramfs/live'])]), disk('/dev/loop0', kind='loop', mounts=['/'])]}
media = {'source': '/dev/vdb1', 'target': '/run/initramfs/live', 'fstype': 'iso9660', 'maj:min': data['blockdevices'][1]['children'][0]['maj:min'], 'options': 'ro'}
loop = {'source': '/dev/loop0', 'target': '/', 'fstype': 'squashfs', 'maj:min': data['blockdevices'][2]['maj:min'], 'options': 'ro'}
class ReadOnlyRunner:
def run(self, args):
if args[0] == 'lsblk':
return json.dumps(data)
return json.dumps({'filesystems': [media] if '--target' in args else [media, loop]})
with patch.object(i.SystemProbe, '_loop_backing_identity',
return_value=media['maj:min']):
topo = i.SystemProbe(ReadOnlyRunner()).snapshot(str(payload))
self.assertFalse(topo.component('/dev/vda') & topo.protected)
self.assertIn('/dev/loop0', topo.component('/dev/vdb'))
self.assertTrue(topo.component('/dev/vdb') & topo.protected)
def test_failed_extraction_retains_diagnostic_on_target(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) / 'target'
root.mkdir()
request = i.Request('/dev/vda', 'emil', execute=True, confirm_erase='/dev/vda')
runner = FakeRunner(root, 'unsquashfs-extract')
with self.assertRaises(i.InstallError):
i.install(request, runner, FakeProbe(), root, 'secret', io.StringIO())
diagnostic = root / 'var/log/hydra-install.log'
self.assertTrue(diagnostic.is_file())
self.assertIn('unsquashfs', diagnostic.read_text())
self.assertNotIn('secret', diagnostic.read_text())
class TargetPathTests(unittest.TestCase):
def test_live_mail_parent_symlink_is_resolved_within_target(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / 'etc').mkdir()
(root / 'etc/passwd').write_text('root:x:0:0:root:/root:/bin/bash\n')
(root / 'var/spool').mkdir(parents=True)
(root / 'var/mail').mkdir()
(root / 'var/spool/mail').symlink_to('/var/mail')
i.remove_live_account(root, FakeRunner(root).run)
def test_kernel_version_symlink_is_not_followed_into_host(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) / 'root'
external = Path(tmp) / 'external'
external.mkdir()
(root / 'lib/modules').mkdir(parents=True)
(root / 'lib/modules/6.12.1').symlink_to(external)
(root / 'boot').mkdir()
(root / 'boot/vmlinuz-6.12.1').touch()
with self.assertRaises(i.InstallError):
i.select_kernel(root, None)
class IdentityTests(unittest.TestCase):
def test_conflicting_device_identities_rejected(self):
a, b = disk(), disk('/dev/vdb')
b['maj:min'] = a['maj:min']
with self.assertRaises(i.InstallError):
i.Topology.from_json(json.dumps({'blockdevices': [a, b]}))
class TtyTests(unittest.TestCase):
def test_password_stream_opens_real_nonseekable_tty(self):
master, slave = pty.openpty()
try:
slave_name = os.ttyname(slave)
real_open = open
def tty_open(path, mode):
self.assertEqual(path, '/dev/tty')
return real_open(slave_name, mode, opener=lambda path, flags: os.open(path, flags | os.O_NOCTTY))
with patch('builtins.open', tty_open), patch.object(i.getpass, 'getpass', return_value='test-only-secret'):
self.assertEqual(i.prompt_password(), 'test-only-secret')
finally:
os.close(slave)
os.close(master)
class ReviewRegressionTests(unittest.TestCase):
def request(self):
return i.Request('/dev/vda', 'emil', execute=True, confirm_erase='/dev/vda')
def test_parentless_protected_stacked_root_refuses_all_targets(self):
for kind in ('lvm', 'crypt', 'dm', 'mpath', 'md', 'raid0', 'raid1', 'raid4', 'raid5', 'raid6', 'raid10'):
with self.subTest(kind=kind), self.assertRaisesRegex(i.InstallError, 'backing parent'):
topo = topology(disk(), disk('/dev/dm-0', kind=kind, mounts=['/']))
i.validate_request(self.request(), FakeProbe(topo))
def test_child_relationship_is_not_evidence_of_backing_parent(self):
for kind in ('part', 'lvm', 'crypt', 'raid1'):
node = disk('/dev/missing-backing', kind=kind,
children=[disk('/dev/dm-0', kind='crypt', mounts=['/'])])
with self.subTest(kind=kind), self.assertRaisesRegex(i.InstallError, 'backing parent'):
topo = topology(disk(), node)
i.validate_request(self.request(), FakeProbe(topo))
def test_complete_stack_on_other_disk_preserves_eligible_target(self):
stack = disk('/dev/vdb', children=[disk('/dev/vdb1', kind='part', children=[
disk('/dev/dm-crypt', kind='crypt', children=[disk('/dev/dm-lvm', kind='lvm', mounts=['/'])])])])
topo = topology(disk(), stack)
self.assertEqual(i.validate_request(self.request(), FakeProbe(topo)), '/dev/vda')
self.assertTrue(topo.component('/dev/vdb') & topo.protected)
def test_empty_optical_drive_does_not_block_eligible_target(self):
optical = disk('/dev/sr0', kind='rom')
optical['size'] = 0
topo = topology(disk(), optical)
self.assertEqual(i.validate_request(self.request(), FakeProbe(topo)), '/dev/vda')
self.assertFalse(topo.component('/dev/sr0') & topo.protected)
def test_zero_size_disk_cannot_be_selected(self):
target = disk()
target['size'] = 0
with self.assertRaises(i.InstallError):
topo = topology(target)
i.validate_request(self.request(), FakeProbe(topo))
def test_zero_size_mounted_optical_drive_is_not_treated_as_empty(self):
optical = disk('/dev/sr0', kind='rom', mounts=['/run/initramfs/live'])
optical['size'] = 0
with self.assertRaises(i.InstallError):
topology(disk(), optical)
def test_zero_size_optical_with_separately_listed_dependent_refused(self):
optical = disk('/dev/sr0', kind='rom')
optical['size'] = 0
dependent = disk('/dev/dm-0', kind='crypt', pkname='/dev/sr0', mounts=['/'])
with self.assertRaises(i.InstallError):
topology(disk(), optical, dependent)
def test_zero_size_optical_cannot_be_protected_by_findmnt(self):
optical = disk('/dev/sr0', kind='rom')
optical['size'] = 0
topo = topology(disk(), optical)
with self.assertRaises(i.InstallError):
topo.protect('/dev/sr0')
def test_zero_size_optical_with_dependents_is_not_irrelevant(self):
optical = disk('/dev/sr0', kind='rom', children=[disk('/dev/dm-0', kind='crypt', mounts=['/'])])
optical['size'] = 0
with self.assertRaises(i.InstallError):
topology(disk(), optical)
if __name__ == '__main__':
unittest.main()