From 9e5f33ea5c85efb6db1ff1db234e8b38f730ceab Mon Sep 17 00:00:00 2001 From: David Kubek Date: Sun, 8 Dec 2024 20:33:48 +0100 Subject: [PATCH] Add XFS inhibitor --- .../common/actors/xfsinfoscanner/actor.py | 7 +- .../libraries/xfsinfoscanner.py | 175 ++++++++--- .../tests/unit_test_xfsinfoscanner.py | 276 ++++++++++++------ repos/system_upgrade/common/models/xfsinfo.py | 51 ++++ .../el9toel10/actors/checkoldxfs/actor.py | 24 ++ .../checkoldxfs/libraries/checkoldxfs.py | 94 ++++++ .../checkoldxfs/tests/test_checkoldxfs.py | 67 +++++ 7 files changed, 569 insertions(+), 125 deletions(-) create mode 100644 repos/system_upgrade/common/models/xfsinfo.py create mode 100644 repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py create mode 100644 repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py create mode 100644 repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py diff --git a/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py b/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py index ebc7e17eee..c190867f4f 100644 --- a/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py +++ b/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py @@ -1,6 +1,6 @@ from leapp.actors import Actor from leapp.libraries.actor.xfsinfoscanner import scan_xfs -from leapp.models import StorageInfo, XFSPresence +from leapp.models import StorageInfo, XFSPresence, XFSInfoFacts from leapp.tags import FactsPhaseTag, IPUWorkflowTag @@ -12,11 +12,14 @@ class XFSInfoScanner(Actor): using ftype = 0. The actor will produce a message with the findings. It will contain a list of all XFS mountpoints with ftype = 0 so that those mountpoints can be handled appropriately for the overlayfs that is going to be created. + """ + #FIXME: add XFSInfoFacts to description + name = 'xfs_info_scanner' consumes = (StorageInfo,) - produces = (XFSPresence,) + produces = (XFSPresence, XFSInfoFacts,) tags = (FactsPhaseTag, IPUWorkflowTag,) def process(self): diff --git a/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py b/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py index fafe456eaa..21059ad909 100644 --- a/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py +++ b/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py @@ -1,7 +1,63 @@ +import re import os from leapp.libraries.stdlib import api, CalledProcessError, run -from leapp.models import StorageInfo, XFSPresence +from leapp.models import StorageInfo, XFSPresence, XFSInfo, XFSInfoFacts +from leapp.exceptions import StopActorExecutionError + + +def scan_xfs(): + storage_info_msgs = api.consume(StorageInfo) + storage_info = next(storage_info_msgs, None) + + if list(storage_info_msgs): + api.current_logger().warning('Unexpectedly received more than one StorageInfo message.') + + fstab_data = set() + mount_data = set() + if storage_info: + fstab_data = scan_xfs_fstab(storage_info.fstab) + mount_data = scan_xfs_mount(storage_info.mount) + + mountpoints = fstab_data | mount_data + + xfs_infos = {} + for mountpoint in mountpoints: + content = read_xfs_info(mountpoint) + if content is None: + continue + + xfs_info = parse_xfs_info(content) + xfs_infos[mountpoint] = xfs_info + + mountpoints_ftype0 = [ + mountpoint + for mountpoint in xfs_infos + if is_without_ftype(xfs_infos[mountpoint]) + ] + + # By now, we only have XFS mountpoints and check whether or not it has ftype = 0 + api.produce(XFSPresence( + present=len(mountpoints) > 0, + without_ftype=len(mountpoints_ftype0) > 0, + mountpoints_without_ftype=mountpoints_ftype0, + )) + + api.produce( + XFSInfoFacts( + mountpoints=[ + XFSInfo( + mountpoint=mountpoint, + meta_data=xfs_infos[mountpoint]['meta-data'], + data=xfs_infos[mountpoint]['data'], + naming=xfs_infos[mountpoint]['naming'], + log=xfs_infos[mountpoint]['log'], + realtime=xfs_infos[mountpoint]['realtime'], + ) + for mountpoint in xfs_infos + ] + ) + ) def scan_xfs_fstab(data): @@ -22,43 +78,94 @@ def scan_xfs_mount(data): return mountpoints -def is_xfs_without_ftype(mp): - if not os.path.ismount(mp): - # Check if mp is actually a mountpoint - api.current_logger().warning('{} is not mounted'.format(mp)) - return False +def read_xfs_info(mp): + if not is_mountpoint(mp): + return None + try: - xfs_info = run(['/usr/sbin/xfs_info', '{}'.format(mp)], split=True) + result = run(['/usr/sbin/xfs_info', '{}'.format(mp)], split=True) except CalledProcessError as err: api.current_logger().warning('Error during command execution: {}'.format(err)) - return False + return None - for l in xfs_info['stdout']: - if 'ftype=0' in l: - return True + return result['stdout'] - return False +def is_mountpoint(mp): + if not os.path.ismount(mp): + # Check if mp is actually a mountpoint + api.current_logger().warning('{} is not mounted'.format(mp)) + return False -def scan_xfs(): - storage_info_msgs = api.consume(StorageInfo) - storage_info = next(storage_info_msgs, None) - - if list(storage_info_msgs): - api.current_logger().warning('Unexpectedly received more than one StorageInfo message.') - - fstab_data = set() - mount_data = set() - if storage_info: - fstab_data = scan_xfs_fstab(storage_info.fstab) - mount_data = scan_xfs_mount(storage_info.mount) - - mountpoints = fstab_data | mount_data - mountpoints_ftype0 = list(filter(is_xfs_without_ftype, mountpoints)) - - # By now, we only have XFS mountpoints and check whether or not it has ftype = 0 - api.produce(XFSPresence( - present=len(mountpoints) > 0, - without_ftype=len(mountpoints_ftype0) > 0, - mountpoints_without_ftype=mountpoints_ftype0, - )) + return True + + +def parse_xfs_info(content): + """ + This parser reads the output of the ``xfs_info`` command. + + In general the pattern is:: + + section =sectionkey key1=value1 key2=value2, key3=value3 + = key4=value4 + nextsec =sectionkey sectionvalue key=value otherkey=othervalue + + Sections are continued over lines as per RFC822. The first equals + sign is column-aligned, and the first key=value is too, but the + rest seems to be comma separated. Specifiers come after the first + equals sign, and sometimes have a value property, but sometimes not. + + NOTE: This function is adapted from + https://github.com/RedHatInsights/insights-core/blob/5118165b22a983a6b9c6d90f85cdedaece3f05f9/insights/parsers/xfs_info.py#L126 + """ + + xfs_info = {} + + info_re = re.compile(r'^(?P
[\w-]+)?\s*' + + r'=(?:(?P\S+)(?:\s(?P\w+))?)?' + + r'\s+(?P\w.*\w)$' + ) + keyval_re = re.compile(r'(?P[\w-]+)=(?P\d+(?: blks)?)') + + sect_info = None + + for line in content: + match = info_re.search(line) + if match: + if match.group('section'): + # Change of section - make new sect_info dict and link + sect_info = {} + xfs_info[match.group('section')] = sect_info + if match.group('specifier'): + sect_info['specifier'] = match.group('specifier') + if match.group('specval'): + sect_info['specifier_value'] = match.group('specval') + for key, value in keyval_re.findall(match.group('keyvaldata')): + sect_info[key] = value + + _validate_xfs_info(xfs_info) + + return xfs_info + + +def _validate_xfs_info(xfs_info): + if 'meta-data' not in xfs_info: + raise StopActorExecutionError("No 'meta-data' section found") + if 'specifier' not in xfs_info['meta-data']: + raise StopActorExecutionError("Device specifier not found in meta-data") + if 'data' not in xfs_info: + raise StopActorExecutionError("No 'data' section found") + if 'blocks' not in xfs_info['data']: + raise StopActorExecutionError("'blocks' not defined in data section") + if 'bsize' not in xfs_info['data']: + raise StopActorExecutionError("'bsize' not defined in data section") + if 'log' not in xfs_info: + raise StopActorExecutionError("No 'log' section found") + if 'blocks' not in xfs_info['log']: + raise StopActorExecutionError("'blocks' not defined in log section") + if 'bsize' not in xfs_info['log']: + raise StopActorExecutionError("'bsize' not defined in log section") + + +def is_without_ftype(xfs_info): + return xfs_info['naming'].get('ftype', '') == '0' diff --git a/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py b/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py index 4ac6a0d16f..d302b26833 100644 --- a/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py +++ b/repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py @@ -6,6 +6,117 @@ from leapp.models import FstabEntry, MountEntry, StorageInfo, SystemdMountEntry, XFSPresence +TEST_XFS_INFO_FTYPE1 = """ +meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks + = sectsz=512 attr=2, projid32bit=1 + = crc=1 finobt=0 spinodes=0 +data = bsize=4096 blocks=524288, imaxpct=25 + = sunit=0 swidth=0 blks +naming =version 2 bsize=4096 ascii-ci=0 ftype=1 +log =internal bsize=4096 blocks=2560, version=2 + = sectsz=512 sunit=0 blks, lazy-count=1 +realtime =none extsz=4096 blocks=0, rtextents=0 +""" +TEST_XFS_INFO_FTYPE1_PARSED = { + 'meta-data': { + 'agcount': '4', + 'agsize': '131072 blks', + 'attr': '2', + 'crc': '1', + 'finobt': '0', + 'isize': '512', + 'projid32bit': '1', + 'sectsz': '512', + 'specifier': '/dev/loop0', + 'spinodes': '0' + }, + 'data': { + 'blocks': '524288', + 'bsize': '4096', + 'imaxpct': '25', + 'sunit': '0', + 'swidth': '0 blks' + }, + 'naming': { + 'ascii-ci': '0', + 'bsize': '4096', + 'ftype': '1', + 'specifier': 'version', + 'specifier_value': '2' + }, + 'log': { + 'blocks': '2560', + 'bsize': '4096', + 'lazy-count': '1', + 'sectsz': '512', + 'specifier': 'internal', + 'sunit': '0 blks', + 'version': '2' + }, + 'realtime': { + 'blocks': '0', + 'extsz': '4096', + 'rtextents': '0', + 'specifier': 'none' + }, +} + +TEST_XFS_INFO_FTYPE0 = """ +meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks + = sectsz=512 attr=2, projid32bit=1 + = crc=1 finobt=0 spinodes=0 +data = bsize=4096 blocks=524288, imaxpct=25 + = sunit=0 swidth=0 blks +naming =version 2 bsize=4096 ascii-ci=0 ftype=0 +log =internal bsize=4096 blocks=2560, version=2 + = sectsz=512 sunit=0 blks, lazy-count=1 +realtime =none extsz=4096 blocks=0, rtextents=0 +""" +TEST_XFS_INFO_FTYPE0_PARSED = { + 'meta-data': { + 'agcount': '4', + 'agsize': '131072 blks', + 'attr': '2', + 'crc': '1', + 'finobt': '0', + 'isize': '512', + 'projid32bit': '1', + 'sectsz': '512', + 'specifier': '/dev/loop0', + 'spinodes': '0' + }, + 'data': { + 'blocks': '524288', + 'bsize': '4096', + 'imaxpct': '25', + 'sunit': '0', + 'swidth': '0 blks' + }, + 'naming': { + 'ascii-ci': '0', + 'bsize': '4096', + 'ftype': '0', + 'specifier': 'version', + 'specifier_value': '2' + }, + 'log': { + 'blocks': '2560', + 'bsize': '4096', + 'lazy-count': '1', + 'sectsz': '512', + 'specifier': 'internal', + 'sunit': '0 blks', + 'version': '2' + }, + 'realtime': { + 'blocks': '0', + 'extsz': '4096', + 'rtextents': '0', + 'specifier': 'none' + } +} + + class run_mocked(object): def __init__(self): self.called = 0 @@ -15,29 +126,10 @@ def __call__(self, args, split=True): self.called += 1 self.args = args - with_ftype = {'stdout': [ - "meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks", - " = sectsz=512 attr=2, projid32bit=1", - " = crc=1 finobt=0 spinodes=0", - "data = bsize=4096 blocks=524288, imaxpct=25", - " = sunit=0 swidth=0 blks", - "naming =version 2 bsize=4096 ascii-ci=0 ftype=1", - "log =internal bsize=4096 blocks=2560, version=2", - " = sectsz=512 sunit=0 blks, lazy-count=1", - "realtime =none extsz=4096 blocks=0, rtextents=0"]} - - without_ftype = {'stdout': [ - "meta-data=/dev/loop0 isize=512 agcount=4, agsize=131072 blks", - " = sectsz=512 attr=2, projid32bit=1", - " = crc=1 finobt=0 spinodes=0", - "data = bsize=4096 blocks=524288, imaxpct=25", - " = sunit=0 swidth=0 blks", - "naming =version 2 bsize=4096 ascii-ci=0 ftype=0", - "log =internal bsize=4096 blocks=2560, version=2", - " = sectsz=512 sunit=0 blks, lazy-count=1", - "realtime =none extsz=4096 blocks=0, rtextents=0"]} - - if "/var" in self.args: + with_ftype = {'stdout': TEST_XFS_INFO_FTYPE1.splitlines()} + without_ftype = {'stdout': TEST_XFS_INFO_FTYPE0.splitlines()} + + if '/var' in self.args: return without_ftype return with_ftype @@ -45,81 +137,76 @@ def __call__(self, args, split=True): def test_scan_xfs_fstab(monkeypatch): fstab_data_no_xfs = { - "fs_spec": "/dev/mapper/fedora-home", - "fs_file": "/home", - "fs_vfstype": "ext4", - "fs_mntops": "defaults,x-systemd.device-timeout=0", - "fs_freq": "1", - "fs_passno": "2"} + 'fs_spec': '/dev/mapper/fedora-home', + 'fs_file': '/home', + 'fs_vfstype': 'ext4', + 'fs_mntops': 'defaults,x-systemd.device-timeout=0', + 'fs_freq': '1', + 'fs_passno': '2'} mountpoints = xfsinfoscanner.scan_xfs_fstab([FstabEntry(**fstab_data_no_xfs)]) assert not mountpoints fstab_data_xfs = { - "fs_spec": "/dev/mapper/rhel-root", - "fs_file": "/", - "fs_vfstype": "xfs", - "fs_mntops": "defaults", - "fs_freq": "0", - "fs_passno": "0"} + 'fs_spec': '/dev/mapper/rhel-root', + 'fs_file': '/', + 'fs_vfstype': 'xfs', + 'fs_mntops': 'defaults', + 'fs_freq': '0', + 'fs_passno': '0'} mountpoints = xfsinfoscanner.scan_xfs_fstab([FstabEntry(**fstab_data_xfs)]) - assert mountpoints == {"/"} + assert mountpoints == {'/'} def test_scan_xfs_mount(monkeypatch): mount_data_no_xfs = { - "name": "tmpfs", - "mount": "/run/snapd/ns", - "tp": "tmpfs", - "options": "rw,nosuid,nodev,seclabel,mode=755"} + 'name': 'tmpfs', + 'mount': '/run/snapd/ns', + 'tp': 'tmpfs', + 'options': 'rw,nosuid,nodev,seclabel,mode=755'} mountpoints = xfsinfoscanner.scan_xfs_mount([MountEntry(**mount_data_no_xfs)]) assert not mountpoints mount_data_xfs = { - "name": "/dev/vda1", - "mount": "/boot", - "tp": "xfs", - "options": "rw,relatime,seclabel,attr2,inode64,noquota"} + 'name': '/dev/vda1', + 'mount': '/boot', + 'tp': 'xfs', + 'options': 'rw,relatime,seclabel,attr2,inode64,noquota'} mountpoints = xfsinfoscanner.scan_xfs_mount([MountEntry(**mount_data_xfs)]) - assert mountpoints == {"/boot"} - - -def test_is_xfs_without_ftype(monkeypatch): - monkeypatch.setattr(xfsinfoscanner, "run", run_mocked()) - monkeypatch.setattr(os.path, "ismount", lambda _: True) + assert mountpoints == {'/boot'} - assert xfsinfoscanner.is_xfs_without_ftype("/var") - assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /var" - assert not xfsinfoscanner.is_xfs_without_ftype("/boot") - assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /boot" +def test_is_without_ftype(monkeypatch): + assert xfsinfoscanner.is_without_ftype(TEST_XFS_INFO_FTYPE0_PARSED) + assert not xfsinfoscanner.is_without_ftype(TEST_XFS_INFO_FTYPE1_PARSED) + assert not xfsinfoscanner.is_without_ftype({'naming': {}}) -def test_is_xfs_command_failed(monkeypatch): +def test_read_xfs_info_failed(monkeypatch): def _run_mocked_exception(*args, **kwargs): - raise CalledProcessError(message="No such file or directory", command=["xfs_info", "/nosuchmountpoint"], + raise CalledProcessError(message='No such file or directory', command=['xfs_info', '/nosuchmountpoint'], result=1) # not a mountpoint - monkeypatch.setattr(os.path, "ismount", lambda _: False) - monkeypatch.setattr(xfsinfoscanner, "run", _run_mocked_exception) - assert not xfsinfoscanner.is_xfs_without_ftype("/nosuchmountpoint") + monkeypatch.setattr(os.path, 'ismount', lambda _: False) + monkeypatch.setattr(xfsinfoscanner, 'run', _run_mocked_exception) + assert xfsinfoscanner.read_xfs_info('/nosuchmountpoint') is None # a real mountpoint but something else caused command to fail - monkeypatch.setattr(os.path, "ismount", lambda _: True) - assert not xfsinfoscanner.is_xfs_without_ftype("/nosuchmountpoint") + monkeypatch.setattr(os.path, 'ismount', lambda _: True) + assert xfsinfoscanner.read_xfs_info('/nosuchmountpoint') is None def test_scan_xfs(monkeypatch): - monkeypatch.setattr(xfsinfoscanner, "run", run_mocked()) - monkeypatch.setattr(os.path, "ismount", lambda _: True) + monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked()) + monkeypatch.setattr(os.path, 'ismount', lambda _: True) def consume_no_xfs_message_mocked(*models): yield StorageInfo() - monkeypatch.setattr(api, "consume", consume_no_xfs_message_mocked) - monkeypatch.setattr(api, "produce", produce_mocked()) + monkeypatch.setattr(api, 'consume', consume_no_xfs_message_mocked) + monkeypatch.setattr(api, 'produce', produce_mocked()) xfsinfoscanner.scan_xfs() assert api.produce.called == 1 @@ -131,14 +218,14 @@ def consume_no_xfs_message_mocked(*models): def consume_ignored_xfs_message_mocked(*models): mount_data = { - "name": "/dev/vda1", - "mount": "/boot", - "tp": "xfs", - "options": "rw,relatime,seclabel,attr2,inode64,noquota"} + 'name': '/dev/vda1', + 'mount': '/boot', + 'tp': 'xfs', + 'options': 'rw,relatime,seclabel,attr2,inode64,noquota'} yield StorageInfo(mount=[MountEntry(**mount_data)]) - monkeypatch.setattr(api, "consume", consume_ignored_xfs_message_mocked) - monkeypatch.setattr(api, "produce", produce_mocked()) + monkeypatch.setattr(api, 'consume', consume_ignored_xfs_message_mocked) + monkeypatch.setattr(api, 'produce', produce_mocked()) xfsinfoscanner.scan_xfs() assert api.produce.called == 1 @@ -150,16 +237,16 @@ def consume_ignored_xfs_message_mocked(*models): def consume_xfs_with_ftype_message_mocked(*models): fstab_data = { - "fs_spec": "/dev/mapper/rhel-root", - "fs_file": "/", - "fs_vfstype": "xfs", - "fs_mntops": "defaults", - "fs_freq": "0", - "fs_passno": "0"} + 'fs_spec': '/dev/mapper/rhel-root', + 'fs_file': '/', + 'fs_vfstype': 'xfs', + 'fs_mntops': 'defaults', + 'fs_freq': '0', + 'fs_passno': '0'} yield StorageInfo(fstab=[FstabEntry(**fstab_data)]) - monkeypatch.setattr(api, "consume", consume_xfs_with_ftype_message_mocked) - monkeypatch.setattr(api, "produce", produce_mocked()) + monkeypatch.setattr(api, 'consume', consume_xfs_with_ftype_message_mocked) + monkeypatch.setattr(api, 'produce', produce_mocked()) xfsinfoscanner.scan_xfs() assert api.produce.called == 1 @@ -171,16 +258,16 @@ def consume_xfs_with_ftype_message_mocked(*models): def consume_xfs_without_ftype_message_mocked(*models): fstab_data = { - "fs_spec": "/dev/mapper/rhel-root", - "fs_file": "/var", - "fs_vfstype": "xfs", - "fs_mntops": "defaults", - "fs_freq": "0", - "fs_passno": "0"} + 'fs_spec': '/dev/mapper/rhel-root', + 'fs_file': '/var', + 'fs_vfstype': 'xfs', + 'fs_mntops': 'defaults', + 'fs_freq': '0', + 'fs_passno': '0'} yield StorageInfo(fstab=[FstabEntry(**fstab_data)]) - monkeypatch.setattr(api, "consume", consume_xfs_without_ftype_message_mocked) - monkeypatch.setattr(api, "produce", produce_mocked()) + monkeypatch.setattr(api, 'consume', consume_xfs_without_ftype_message_mocked) + monkeypatch.setattr(api, 'produce', produce_mocked()) xfsinfoscanner.scan_xfs() assert api.produce.called == 1 @@ -195,8 +282,8 @@ def consume_xfs_without_ftype_message_mocked(*models): def consume_no_message_mocked(*models): yield None - monkeypatch.setattr(api, "consume", consume_no_message_mocked) - monkeypatch.setattr(api, "produce", produce_mocked()) + monkeypatch.setattr(api, 'consume', consume_no_message_mocked) + monkeypatch.setattr(api, 'produce', produce_mocked()) xfsinfoscanner.scan_xfs() assert api.produce.called == 1 @@ -205,3 +292,14 @@ def consume_no_message_mocked(*models): assert not api.produce.model_instances[0].present assert not api.produce.model_instances[0].without_ftype assert not api.produce.model_instances[0].mountpoints_without_ftype + + # TODO(dkubek) add checks for XFSInfoFacts + assert False + + +def test_parse_xfs_info(monkeypatch): + xfs_info = xfsinfoscanner.parse_xfs_info(TEST_XFS_INFO_FTYPE0.splitlines()) + assert xfs_info == TEST_XFS_INFO_FTYPE0_PARSED + + xfs_info = xfsinfoscanner.parse_xfs_info(TEST_XFS_INFO_FTYPE1.splitlines()) + assert xfs_info == TEST_XFS_INFO_FTYPE1_PARSED diff --git a/repos/system_upgrade/common/models/xfsinfo.py b/repos/system_upgrade/common/models/xfsinfo.py new file mode 100644 index 0000000000..fada53cdcd --- /dev/null +++ b/repos/system_upgrade/common/models/xfsinfo.py @@ -0,0 +1,51 @@ +from leapp.models import fields, Model +from leapp.topics import SystemInfoTopic + + +class XFSInfo(Model): + """ + A message containing the parsed results from `xfs_info` command for given mountpoint. + + Attributes are stored as key-value pairs. Optional section attribute is + stored under the identifier 'specifier'. + """ + topic = SystemInfoTopic + + mountpoint = fields.String() + """ + Mountpoint containing the XFS filesystem. + """ + + meta_data = fields.StringMap(fields.String()) + """ + Attributes of 'meta-data' section. + """ + + data = fields.StringMap(fields.String()) + """ + Attributes of 'data' section. + """ + + naming = fields.StringMap(fields.String()) + """ + Attributes of 'naming' section. + """ + + log = fields.StringMap(fields.String()) + """ + Attributes of 'log' section. + """ + + realtime = fields.StringMap(fields.String()) + """ + Attributes of 'realtime' section. + """ + + +class XFSInfoFacts(Model): + """ + Message containing the xfs info for all mounted XFS filesystems. + """ + topic = SystemInfoTopic + + mountpoints = fields.List(fields.Model(XFSInfo)) diff --git a/repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py b/repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py new file mode 100644 index 0000000000..0355ac4562 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py @@ -0,0 +1,24 @@ +import leapp.libraries.actor.checkoldxfs as checkoldxfs +from leapp.actors import Actor +from leapp.reporting import Report +from leapp.models import XFSInfoFacts +from leapp.tags import ChecksPhaseTag, IPUWorkflowTag + + +class CheckOldXFS(Actor): + """ + Inhibit upgrade if XFS requirements for RHEL 10 are not satisfied. + + RHEL 10 introduces stricter requirements for XFS filesystems. If any XFS + filesystem on the system lack these required features, the upgrade will be + inhibited. + + """ + + name = 'check_old_xfs' + consumes = (XFSInfoFacts,) + produces = (Report,) + tags = (ChecksPhaseTag, IPUWorkflowTag,) + + def process(self): + checkoldxfs.process() diff --git a/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py b/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py new file mode 100644 index 0000000000..a1be33defb --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py @@ -0,0 +1,94 @@ +from leapp import reporting +from leapp.exceptions import StopActorExecutionError +from leapp.libraries.stdlib import api +from leapp.models import XFSInfoFacts + + +def process(): + xfs_info_facts = _get_xfs_info_facts() + + invalid_bigtime = [] + invalid_crc = [] + for xfs_info in xfs_info_facts.mountpoints: + if not _has_valid_bigtime(xfs_info): + api.current_logger().debug( + 'Mountpoint {} has invalid bigtime'.format(xfs_info.mountpoint)) + invalid_bigtime.append(xfs_info.mountpoint) + + if not _has_valid_crc(xfs_info): + api.current_logger().debug( + 'Mountpoint {} has invalid crc'.format(xfs_info.mountpoint)) + invalid_crc.append(xfs_info.mountpoint) + + if invalid_bigtime or invalid_crc: + _inhibit_upgrade(invalid_bigtime, invalid_crc) + + return + + api.current_logger().debug('All XFS system detected are valid.') + + +def _get_xfs_info_facts(): + msgs = api.consume(XFSInfoFacts) + + xfs_info_facts = next(msgs, None) + if xfs_info_facts is None: + raise StopActorExecutionError('Could not retrieve XFSInfoFacts!') + + if next(msgs, None): + api.current_logger().warning( + 'Unexpectedly received more than one XFSInfoFacts message.') + + return xfs_info_facts + + +def _has_valid_bigtime(xfs_info): + return xfs_info.meta_data.get('bigtime', '') == '1' + + +def _has_valid_crc(xfs_info): + return xfs_info.meta_data.get('crc', '') == '1' + + +def _inhibit_upgrade(invalid_bigtime, invalid_crc): + title = 'Upgrade to RHEL 10 inhibited due to incompatible XFS filesystems.' + summary = ''.join([ + ( + 'Some XFS filesystems on this system are incompatible with the RHEL 10' + ' requirements. Specifically, the following issues were found:\n' + ), + ('Filesystems with invalid "bigtime": {}\n'.format( + ', '.join(invalid_bigtime)) if invalid_bigtime else ''), + ('Filesystems with invalid "crc": {}\n'.format( + ', '.join(invalid_crc)) if invalid_crc else ''), + ]) + + + remediation_hint = ( + 'To address this issue:\n' + '- Enable the "bigtime" feature on v5 filesystems using the command:\n' + ' # xfs_admin -O bigtime=1 \n' + '- Migrate filesystems to new version of XFS' + ' This involves:\n' + ' 1. Backing up the filesystem data.\n' + ' 2. Reformatting the filesystem with the "ftype=1" option.\n' + ' 3. Restoring the data from the backup.\n' + '\n' + 'For root filesystems, a clean installation is recommended to ensure' + ' compatibility. For user data filesystems, perform a backup, reformat,' + ' and restore procedure. Refer to official documentation for detailed' + ' guidance.' + ) + + reporting.create_report([ + reporting.Title(title), + reporting.Summary(summary), + reporting.ExternalLink( + title='Guidance on upgrading XFS filesystems for RHEL 10', + # FIXME: Create short URL + url='https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/managing_file_systems/restoring-an-xfs-file-system-from-backup_managing-file-systems' + ), + reporting.Severity(reporting.Severity.HIGH), + reporting.Groups([reporting.Groups.INHIBITOR]), + reporting.Remediation(hint=remediation_hint), + ]) diff --git a/repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py b/repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py new file mode 100644 index 0000000000..97c01e7701 --- /dev/null +++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py @@ -0,0 +1,67 @@ +import pytest + +from leapp import reporting +from leapp.libraries.actor import checkarmbootloader +from leapp.libraries.common.config.architecture import ARCH_ARM64, ARCH_SUPPORTED +from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked +from leapp.libraries.stdlib import api +from leapp.utils.report import is_inhibitor + + +@pytest.mark.parametrize("arch", [arch for arch in ARCH_SUPPORTED if not arch == ARCH_ARM64]) +def test_not_x86_64_passes(monkeypatch, arch): + """ + Test no report is generated on an architecture different from ARM + """ + + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=arch)) + + checkarmbootloader.process() + + assert 'Architecture not ARM.' in api.current_logger.infomsg[0] + assert not reporting.create_report.called + + +@pytest.mark.parametrize("target_version", ["9.2", "9.4"]) +def test_valid_path(monkeypatch, target_version): + """ + Test no report is generated on a supported path + """ + + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + monkeypatch.setattr( + api, 'current_actor', + CurrentActorMocked(arch=ARCH_ARM64, src_ver='8.10', dst_ver=target_version) + ) + + checkarmbootloader.process() + + assert 'Upgrade on ARM architecture on a compatible path' in api.current_logger.infomsg[0] + assert not reporting.create_report.called + + +def test_invalid_path(monkeypatch): + """ + Test report is generated on a invalid upgrade path + """ + + monkeypatch.setattr(reporting, "create_report", create_report_mocked()) + monkeypatch.setattr(api, 'current_logger', logger_mocked()) + monkeypatch.setattr( + api, 'current_actor', + CurrentActorMocked(arch=ARCH_ARM64, src_ver='8.10', dst_ver='9.5') + ) + + checkarmbootloader.process() + + produced_title = reporting.create_report.report_fields.get('title') + produced_summary = reporting.create_report.report_fields.get('summary') + + assert reporting.create_report.called == 1 + assert 'not possible for ARM machines' in produced_title + assert 'Due to the incompatibility' in produced_summary + assert reporting.create_report.report_fields['severity'] == reporting.Severity.HIGH + assert is_inhibitor(reporting.create_report.report_fields)