fix(installer): identify live loop backing after dracut switch-root
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"filesystems": [
|
||||
{
|
||||
"source": "LiveOS_rootfs",
|
||||
"target": "/",
|
||||
"fstype": "overlay",
|
||||
"maj:min": "0:31",
|
||||
"options": "rw,relatime,lowerdir=/run/rootfsbase,upperdir=/run/overlayfs,workdir=/run/ovlwork,uuid=on",
|
||||
"children": [
|
||||
{
|
||||
"source": "tmpfs",
|
||||
"target": "/tmp",
|
||||
"fstype": "tmpfs",
|
||||
"maj:min": "0:40",
|
||||
"options": "rw,nosuid,nodev,nr_inodes=1048576,inode64,usrquota"
|
||||
},
|
||||
{
|
||||
"source": "devtmpfs",
|
||||
"target": "/dev",
|
||||
"fstype": "devtmpfs",
|
||||
"maj:min": "0:6",
|
||||
"options": "rw,nosuid,size=1930828k,nr_inodes=482707,mode=755,inode64",
|
||||
"children": [
|
||||
{
|
||||
"source": "hugetlbfs",
|
||||
"target": "/dev/hugepages",
|
||||
"fstype": "hugetlbfs",
|
||||
"maj:min": "0:36",
|
||||
"options": "rw,nosuid,nodev,relatime,pagesize=2M"
|
||||
},
|
||||
{
|
||||
"source": "mqueue",
|
||||
"target": "/dev/mqueue",
|
||||
"fstype": "mqueue",
|
||||
"maj:min": "0:21",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
},
|
||||
{
|
||||
"source": "tmpfs",
|
||||
"target": "/dev/shm",
|
||||
"fstype": "tmpfs",
|
||||
"maj:min": "0:24",
|
||||
"options": "rw,nosuid,nodev,inode64,usrquota"
|
||||
},
|
||||
{
|
||||
"source": "devpts",
|
||||
"target": "/dev/pts",
|
||||
"fstype": "devpts",
|
||||
"maj:min": "0:25",
|
||||
"options": "rw,nosuid,noexec,relatime,gid=5,mode=600,ptmxmode=000"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "sysfs",
|
||||
"target": "/sys",
|
||||
"fstype": "sysfs",
|
||||
"maj:min": "0:23",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime",
|
||||
"children": [
|
||||
{
|
||||
"source": "fusectl",
|
||||
"target": "/sys/fs/fuse/connections",
|
||||
"fstype": "fusectl",
|
||||
"maj:min": "0:37",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
},
|
||||
{
|
||||
"source": "tracefs",
|
||||
"target": "/sys/kernel/tracing",
|
||||
"fstype": "tracefs",
|
||||
"maj:min": "0:13",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
},
|
||||
{
|
||||
"source": "debugfs",
|
||||
"target": "/sys/kernel/debug",
|
||||
"fstype": "debugfs",
|
||||
"maj:min": "0:8",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
},
|
||||
{
|
||||
"source": "securityfs",
|
||||
"target": "/sys/kernel/security",
|
||||
"fstype": "securityfs",
|
||||
"maj:min": "0:7",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
},
|
||||
{
|
||||
"source": "cgroup2",
|
||||
"target": "/sys/fs/cgroup",
|
||||
"fstype": "cgroup2",
|
||||
"maj:min": "0:27",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot,memory_hugetlb_accounting"
|
||||
},
|
||||
{
|
||||
"source": "none",
|
||||
"target": "/sys/fs/pstore",
|
||||
"fstype": "pstore",
|
||||
"maj:min": "0:28",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
},
|
||||
{
|
||||
"source": "efivarfs",
|
||||
"target": "/sys/firmware/efi/efivars",
|
||||
"fstype": "efivarfs",
|
||||
"maj:min": "0:29",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
},
|
||||
{
|
||||
"source": "bpf",
|
||||
"target": "/sys/fs/bpf",
|
||||
"fstype": "bpf",
|
||||
"maj:min": "0:30",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime,mode=700"
|
||||
},
|
||||
{
|
||||
"source": "configfs",
|
||||
"target": "/sys/kernel/config",
|
||||
"fstype": "configfs",
|
||||
"maj:min": "0:19",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "proc",
|
||||
"target": "/proc",
|
||||
"fstype": "proc",
|
||||
"maj:min": "0:22",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime",
|
||||
"children": [
|
||||
{
|
||||
"source": "systemd-1",
|
||||
"target": "/proc/sys/fs/binfmt_misc",
|
||||
"fstype": "autofs",
|
||||
"maj:min": "0:33",
|
||||
"options": "rw,relatime,fd=38,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=5489",
|
||||
"children": [
|
||||
{
|
||||
"source": "binfmt_misc",
|
||||
"target": "/proc/sys/fs/binfmt_misc",
|
||||
"fstype": "binfmt_misc",
|
||||
"maj:min": "0:42",
|
||||
"options": "rw,nosuid,nodev,noexec,relatime"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "tmpfs",
|
||||
"target": "/run",
|
||||
"fstype": "tmpfs",
|
||||
"maj:min": "0:26",
|
||||
"options": "rw,nosuid,nodev,size=797732k,nr_inodes=819200,mode=755,inode64",
|
||||
"children": [
|
||||
{
|
||||
"source": "none",
|
||||
"target": "/run/credentials/systemd-journald.service",
|
||||
"fstype": "tmpfs",
|
||||
"maj:min": "0:35",
|
||||
"options": "ro,nosuid,nodev,noexec,relatime,nosymfollow,size=1024k,nr_inodes=1024,mode=700,inode64,noswap"
|
||||
},
|
||||
{
|
||||
"source": "tmpfs",
|
||||
"target": "/run/user/1000",
|
||||
"fstype": "tmpfs",
|
||||
"maj:min": "0:46",
|
||||
"options": "rw,nosuid,nodev,relatime,size=398864k,nr_inodes=99716,mode=700,uid=1000,gid=1000,inode64"
|
||||
},
|
||||
{
|
||||
"source": "/dev/sr0",
|
||||
"target": "/run/initramfs/live",
|
||||
"fstype": "iso9660",
|
||||
"maj:min": "11:0",
|
||||
"options": "ro,relatime,nojoliet,check=s,map=n,blocksize=2048,iocharset=utf8"
|
||||
},
|
||||
{
|
||||
"source": "/dev/loop0",
|
||||
"target": "/run/rootfsbase",
|
||||
"fstype": "squashfs",
|
||||
"maj:min": "7:0",
|
||||
"options": "ro,relatime,errors=continue"
|
||||
},
|
||||
{
|
||||
"source": "none",
|
||||
"target": "/run/credentials/getty@tty2.service",
|
||||
"fstype": "tmpfs",
|
||||
"maj:min": "0:47",
|
||||
"options": "ro,nosuid,nodev,noexec,relatime,nosymfollow,size=1024k,nr_inodes=1024,mode=700,inode64,noswap"
|
||||
},
|
||||
{
|
||||
"source": "none",
|
||||
"target": "/run/credentials/serial-getty@ttyS0.service",
|
||||
"fstype": "tmpfs",
|
||||
"maj:min": "0:48",
|
||||
"options": "ro,nosuid,nodev,noexec,relatime,nosymfollow,size=1024k,nr_inodes=1024,mode=700,inode64,noswap"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"blockdevices": [
|
||||
{
|
||||
"path": "/dev/loop0",
|
||||
"type": "loop",
|
||||
"size": 2220142592,
|
||||
"maj:min": "7:0",
|
||||
"pkname": null,
|
||||
"mountpoints": [
|
||||
"/run/rootfsbase"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/dev/sr0",
|
||||
"type": "rom",
|
||||
"size": 2345107456,
|
||||
"maj:min": "11:0",
|
||||
"pkname": null,
|
||||
"mountpoints": [
|
||||
"/run/initramfs/live"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/dev/sr1",
|
||||
"type": "rom",
|
||||
"size": 1073741312,
|
||||
"maj:min": "11:1",
|
||||
"pkname": null,
|
||||
"mountpoints": []
|
||||
},
|
||||
{
|
||||
"path": "/dev/vda",
|
||||
"type": "disk",
|
||||
"size": 34359738368,
|
||||
"maj:min": "253:0",
|
||||
"pkname": null,
|
||||
"mountpoints": []
|
||||
}
|
||||
]
|
||||
}
|
||||
+85
-6
@@ -4,6 +4,9 @@ import io
|
||||
import json
|
||||
import os
|
||||
import pty
|
||||
import stat
|
||||
import struct
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -403,6 +406,86 @@ class AdditionalFailureTests(unittest.TestCase):
|
||||
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
|
||||
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 SnapshotTests(unittest.TestCase):
|
||||
def test_snapshot_connects_live_loop_to_its_physical_backing(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -416,12 +499,8 @@ class SnapshotTests(unittest.TestCase):
|
||||
if args[0] == 'lsblk':
|
||||
return json.dumps(data)
|
||||
return json.dumps({'filesystems': [media] if '--target' in args else [media, loop]})
|
||||
real_read = Path.read_text
|
||||
def read(path, *args, **kw):
|
||||
if str(path).startswith('/sys/dev/block/'):
|
||||
return str(payload)
|
||||
return real_read(path, *args, **kw)
|
||||
with patch.object(Path, 'read_text', read):
|
||||
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'))
|
||||
|
||||
Reference in New Issue
Block a user