- Detect XFS file systems with problematic parameters - Raise an inhibitor if unsupported target version supplied instead of error - Prevent a possible crash with LiveMode when adding the upgrade boot entry on systems with LVM - Resolves: RHEL-57043, RHEL-52309, RHEL-60034
1403 lines
51 KiB
Diff
1403 lines
51 KiB
Diff
From c5accf4fc657f31ecb70148bf34d2875d1633a9c Mon Sep 17 00:00:00 2001
|
|
From: David Kubek <dkubek@redhat.com>
|
|
Date: Sun, 8 Dec 2024 20:33:48 +0100
|
|
Subject: [PATCH 63/63] Add inhibitor for unsupported XFS
|
|
|
|
Kernel in RHEL 10 drops support of XFS v4 format file systems and such
|
|
file systems will not be possible to mount there anymore. Also, RHEL 10
|
|
systems will be challenged by Y2K38 problem. XFS file system resolves
|
|
that by `bigtime` feature, however, this feature could be manually
|
|
disabled when creating the file system and mainly it's available since
|
|
RHEL 9. So all XFS file systems created earlier will not have this
|
|
enabled - unless users do it on RHEL 9 manually. Note that this will be
|
|
problem for all systems with XFS which upgraded from previous RHEL
|
|
versions.
|
|
|
|
For this reason, inhibit the upgrade if any mounted XFS file systems
|
|
have old XFS v4 format (detect `crc=0`).
|
|
|
|
Instead of inhibiting upgrade when the `bigtime` feature is disabled
|
|
(`bigtime=0` or missing) a low severity report is created as there is
|
|
still time until this issue will be present and other solutions are
|
|
being worked on.
|
|
|
|
Introduces new model `XFSInfoFacts` which collects parsed information
|
|
about all mounted XFS file systems. Note that as we use
|
|
only a few values from `xfs_info` utility, models specify now just this
|
|
limited amount of values as well to limit the burden of maintanance and
|
|
risk of issues. However expected design of the model is already prepared
|
|
and other expected fields are commented out in the code to make the
|
|
further extension in future more simple for others. All values of XFS
|
|
attributes are now represented as strings.
|
|
|
|
JIRA: RHELMISC-8212, RHEL-60034, RHEL-52309
|
|
---
|
|
.../common/actors/xfsinfoscanner/actor.py | 24 +-
|
|
.../libraries/xfsinfoscanner.py | 202 ++++++--
|
|
.../tests/unit_test_xfsinfoscanner.py | 432 +++++++++++++-----
|
|
repos/system_upgrade/common/models/xfsinfo.py | 147 ++++++
|
|
.../el9toel10/actors/checkoldxfs/actor.py | 34 ++
|
|
.../checkoldxfs/libraries/checkoldxfs.py | 141 ++++++
|
|
.../checkoldxfs/tests/test_checkoldxfs.py | 214 +++++++++
|
|
7 files changed, 1027 insertions(+), 167 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 ebc7e17e..a97e51fa 100644
|
|
--- a/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py
|
|
+++ b/repos/system_upgrade/common/actors/xfsinfoscanner/actor.py
|
|
@@ -1,22 +1,32 @@
|
|
from leapp.actors import Actor
|
|
from leapp.libraries.actor.xfsinfoscanner import scan_xfs
|
|
-from leapp.models import StorageInfo, XFSPresence
|
|
+from leapp.models import StorageInfo, XFSInfoFacts, XFSPresence
|
|
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
|
|
|
|
|
|
class XFSInfoScanner(Actor):
|
|
"""
|
|
- This actor scans all mounted mountpoints for XFS information
|
|
+ This actor scans all mounted mountpoints for XFS information.
|
|
+
|
|
+ The actor checks the `StorageInfo` message, which contains details about
|
|
+ the system's storage. For each mountpoint reported, it determines whether
|
|
+ the filesystem is XFS and collects information about its configuration.
|
|
+ Specifically, it identifies whether the XFS filesystem is using `ftype=0`,
|
|
+ which requires special handling for overlay filesystems.
|
|
+
|
|
+ The actor produces two types of messages:
|
|
+
|
|
+ - `XFSPresence`: Indicates whether any XFS use `ftype=0`, and lists the
|
|
+ mountpoints where `ftype=0` is used.
|
|
+
|
|
+ - `XFSInfoFacts`: Contains detailed metadata about all XFS mountpoints.
|
|
+ This includes sections parsed from the `xfs_info` command.
|
|
|
|
- The actor will check each mountpoint reported in the StorageInfo message, if the mountpoint is a partition with XFS
|
|
- 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.
|
|
"""
|
|
|
|
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 fafe456e..7e4de355 100644
|
|
--- a/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py
|
|
+++ b/repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py
|
|
@@ -1,13 +1,74 @@
|
|
import os
|
|
+import re
|
|
|
|
from leapp.libraries.stdlib import api, CalledProcessError, run
|
|
-from leapp.models import StorageInfo, XFSPresence
|
|
+from leapp.models import (
|
|
+ StorageInfo,
|
|
+ XFSInfo,
|
|
+ XFSInfoData,
|
|
+ XFSInfoFacts,
|
|
+ XFSInfoLog,
|
|
+ XFSInfoMetaData,
|
|
+ XFSInfoNaming,
|
|
+ XFSInfoRealtime,
|
|
+ XFSPresence
|
|
+)
|
|
+
|
|
+
|
|
+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=[
|
|
+ generate_xfsinfo_for_mountpoint(xfs_infos[mountpoint], mountpoint)
|
|
+ for mountpoint in xfs_infos
|
|
+ ]
|
|
+ )
|
|
+ )
|
|
|
|
|
|
def scan_xfs_fstab(data):
|
|
mountpoints = set()
|
|
for entry in data:
|
|
- if entry.fs_vfstype == "xfs":
|
|
+ if entry.fs_vfstype == 'xfs':
|
|
mountpoints.add(entry.fs_file)
|
|
|
|
return mountpoints
|
|
@@ -16,49 +77,116 @@ def scan_xfs_fstab(data):
|
|
def scan_xfs_mount(data):
|
|
mountpoints = set()
|
|
for entry in data:
|
|
- if entry.tp == "xfs":
|
|
+ if entry.tp == 'xfs':
|
|
mountpoints.add(entry.mount)
|
|
|
|
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
|
|
-
|
|
- for l in xfs_info['stdout']:
|
|
- if 'ftype=0' in l:
|
|
- return True
|
|
-
|
|
- return False
|
|
-
|
|
+ api.current_logger().warning(
|
|
+ 'Error during command execution: {}'.format(err)
|
|
+ )
|
|
+ return None
|
|
|
|
-def scan_xfs():
|
|
- storage_info_msgs = api.consume(StorageInfo)
|
|
- storage_info = next(storage_info_msgs, None)
|
|
+ return result['stdout']
|
|
|
|
- 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))
|
|
+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
|
|
|
|
- # 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 [1]
|
|
+
|
|
+ [1]: https://github.com/RedHatInsights/insights-core/blob/master/insights/parsers/xfs_info.py
|
|
+ """
|
|
+
|
|
+ xfs_info = {}
|
|
+
|
|
+ info_re = re.compile(r'^(?P<section>[\w-]+)?\s*' +
|
|
+ r'=(?:(?P<specifier>\S+)(?:\s(?P<specval>\w+))?)?' +
|
|
+ r'\s+(?P<keyvaldata>\w.*\w)$'
|
|
+ )
|
|
+ keyval_re = re.compile(r'(?P<key>[\w-]+)=(?P<value>\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
|
|
+
|
|
+ # Normalize strings
|
|
+ xfs_info = {
|
|
+ str(section): {
|
|
+ str(attr): str(value)
|
|
+ for attr, value in sect_info.items()
|
|
+ }
|
|
+ for section, sect_info in xfs_info.items()
|
|
+ }
|
|
+
|
|
+ return xfs_info
|
|
+
|
|
+
|
|
+def is_without_ftype(xfs_info):
|
|
+ return xfs_info['naming'].get('ftype', '') == '0'
|
|
+
|
|
+
|
|
+def generate_xfsinfo_for_mountpoint(xfs_info, mountpoint):
|
|
+ result = XFSInfo(
|
|
+ mountpoint=mountpoint,
|
|
+ meta_data=XFSInfoMetaData(
|
|
+ device=xfs_info['meta-data']['specifier'],
|
|
+ bigtime=xfs_info['meta-data'].get('bigtime'),
|
|
+ crc=xfs_info['meta-data'].get('crc'),
|
|
+ ),
|
|
+ data=XFSInfoData(
|
|
+ bsize=xfs_info['data']['bsize'],
|
|
+ blocks=xfs_info['data']['blocks']
|
|
+ ),
|
|
+ naming=XFSInfoNaming(
|
|
+ ftype=xfs_info['naming']['ftype']
|
|
+ ),
|
|
+ log=XFSInfoLog(
|
|
+ bsize=xfs_info['log']['bsize'],
|
|
+ blocks=xfs_info['log']['blocks']
|
|
+ ),
|
|
+ realtime=XFSInfoRealtime(),
|
|
+ )
|
|
+
|
|
+ return result
|
|
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 4ac6a0d1..71f46b47 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
|
|
@@ -3,7 +3,147 @@ import os
|
|
from leapp.libraries.actor import xfsinfoscanner
|
|
from leapp.libraries.common.testutils import produce_mocked
|
|
from leapp.libraries.stdlib import api, CalledProcessError
|
|
-from leapp.models import FstabEntry, MountEntry, StorageInfo, SystemdMountEntry, XFSPresence
|
|
+from leapp.models import (
|
|
+ FstabEntry,
|
|
+ MountEntry,
|
|
+ StorageInfo,
|
|
+ SystemdMountEntry,
|
|
+ XFSInfo,
|
|
+ XFSInfoData,
|
|
+ XFSInfoFacts,
|
|
+ XFSInfoLog,
|
|
+ XFSInfoMetaData,
|
|
+ XFSInfoNaming,
|
|
+ XFSInfoRealtime,
|
|
+ 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_FTYPE1_MODEL = XFSInfo(
|
|
+ mountpoint='/',
|
|
+ meta_data=XFSInfoMetaData(device='/dev/loop0', bigtime=None, crc='1'),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(ftype='1'),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime()
|
|
+)
|
|
+
|
|
+
|
|
+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'
|
|
+ }
|
|
+}
|
|
+TEST_XFS_INFO_FTYPE0_MODEL = XFSInfo(
|
|
+ mountpoint='/var',
|
|
+ meta_data=XFSInfoMetaData(device='/dev/loop0', bigtime=None, crc='1'),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(ftype='0'),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime()
|
|
+)
|
|
|
|
|
|
class run_mocked(object):
|
|
@@ -15,29 +155,10 @@ class run_mocked(object):
|
|
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,163 +166,228 @@ class run_mocked(object):
|
|
|
|
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"}
|
|
-
|
|
+ assert mountpoints == {'/boot'}
|
|
|
|
-def test_is_xfs_without_ftype(monkeypatch):
|
|
- monkeypatch.setattr(xfsinfoscanner, "run", run_mocked())
|
|
- monkeypatch.setattr(os.path, "ismount", lambda _: True)
|
|
|
|
- assert xfsinfoscanner.is_xfs_without_ftype("/var")
|
|
- assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /var"
|
|
+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': {}})
|
|
|
|
- assert not xfsinfoscanner.is_xfs_without_ftype("/boot")
|
|
- assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /boot"
|
|
|
|
-
|
|
-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)
|
|
+def test_scan_xfs_no_xfs(monkeypatch):
|
|
+ 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
|
|
- assert len(api.produce.model_instances) == 1
|
|
- assert isinstance(api.produce.model_instances[0], XFSPresence)
|
|
- 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
|
|
+
|
|
+ assert api.produce.called == 2
|
|
+ assert len(api.produce.model_instances) == 2
|
|
+
|
|
+ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence))
|
|
+ assert not xfs_presence.present
|
|
+ assert not xfs_presence.without_ftype
|
|
+ assert not xfs_presence.mountpoints_without_ftype
|
|
+
|
|
+ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts))
|
|
+ assert xfs_info_facts.mountpoints == []
|
|
+
|
|
+
|
|
+def test_scan_xfs_ignored_xfs(monkeypatch):
|
|
+ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked())
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda _: True)
|
|
|
|
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
|
|
- assert len(api.produce.model_instances) == 1
|
|
- assert isinstance(api.produce.model_instances[0], XFSPresence)
|
|
- assert 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
|
|
+
|
|
+ assert api.produce.called == 2
|
|
+ assert len(api.produce.model_instances) == 2
|
|
+
|
|
+ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence))
|
|
+ assert xfs_presence.present
|
|
+ assert not xfs_presence.without_ftype
|
|
+ assert not xfs_presence.mountpoints_without_ftype
|
|
+
|
|
+ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts))
|
|
+ assert len(xfs_info_facts.mountpoints) == 1
|
|
+ assert xfs_info_facts.mountpoints[0].mountpoint == '/boot'
|
|
+ assert xfs_info_facts.mountpoints[0].meta_data == TEST_XFS_INFO_FTYPE1_MODEL.meta_data
|
|
+ assert xfs_info_facts.mountpoints[0].data == TEST_XFS_INFO_FTYPE1_MODEL.data
|
|
+ assert xfs_info_facts.mountpoints[0].naming == TEST_XFS_INFO_FTYPE1_MODEL.naming
|
|
+ assert xfs_info_facts.mountpoints[0].log == TEST_XFS_INFO_FTYPE1_MODEL.log
|
|
+ assert xfs_info_facts.mountpoints[0].realtime == TEST_XFS_INFO_FTYPE1_MODEL.realtime
|
|
+
|
|
+
|
|
+def test_scan_xfs_with_ftype(monkeypatch):
|
|
+ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked())
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda _: True)
|
|
|
|
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
|
|
- assert len(api.produce.model_instances) == 1
|
|
- assert isinstance(api.produce.model_instances[0], XFSPresence)
|
|
- assert 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
|
|
+
|
|
+ assert api.produce.called == 2
|
|
+ assert len(api.produce.model_instances) == 2
|
|
+
|
|
+ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence))
|
|
+ assert xfs_presence.present
|
|
+ assert not xfs_presence.without_ftype
|
|
+ assert not xfs_presence.mountpoints_without_ftype
|
|
+
|
|
+ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts))
|
|
+ assert len(xfs_info_facts.mountpoints) == 1
|
|
+ assert xfs_info_facts.mountpoints[0].mountpoint == '/'
|
|
+ assert xfs_info_facts.mountpoints[0].meta_data == TEST_XFS_INFO_FTYPE1_MODEL.meta_data
|
|
+ assert xfs_info_facts.mountpoints[0].data == TEST_XFS_INFO_FTYPE1_MODEL.data
|
|
+ assert xfs_info_facts.mountpoints[0].naming == TEST_XFS_INFO_FTYPE1_MODEL.naming
|
|
+ assert xfs_info_facts.mountpoints[0].log == TEST_XFS_INFO_FTYPE1_MODEL.log
|
|
+ assert xfs_info_facts.mountpoints[0].realtime == TEST_XFS_INFO_FTYPE1_MODEL.realtime
|
|
+
|
|
+
|
|
+def test_scan_xfs_without_ftype(monkeypatch):
|
|
+ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked())
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda _: True)
|
|
|
|
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
|
|
- assert len(api.produce.model_instances) == 1
|
|
- assert isinstance(api.produce.model_instances[0], XFSPresence)
|
|
- assert api.produce.model_instances[0].present
|
|
- assert api.produce.model_instances[0].without_ftype
|
|
- assert api.produce.model_instances[0].mountpoints_without_ftype
|
|
- assert len(api.produce.model_instances[0].mountpoints_without_ftype) == 1
|
|
- assert api.produce.model_instances[0].mountpoints_without_ftype[0] == '/var'
|
|
+
|
|
+ assert api.produce.called == 2
|
|
+ assert len(api.produce.model_instances) == 2
|
|
+
|
|
+ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence))
|
|
+ assert xfs_presence.present
|
|
+ assert xfs_presence.without_ftype
|
|
+ assert xfs_presence.mountpoints_without_ftype
|
|
+
|
|
+ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts))
|
|
+ assert len(xfs_info_facts.mountpoints) == 1
|
|
+ assert xfs_info_facts.mountpoints[0].mountpoint == '/var'
|
|
+ assert xfs_info_facts.mountpoints[0].meta_data == TEST_XFS_INFO_FTYPE0_MODEL.meta_data
|
|
+ assert xfs_info_facts.mountpoints[0].data == TEST_XFS_INFO_FTYPE0_MODEL.data
|
|
+ assert xfs_info_facts.mountpoints[0].naming == TEST_XFS_INFO_FTYPE0_MODEL.naming
|
|
+ assert xfs_info_facts.mountpoints[0].log == TEST_XFS_INFO_FTYPE0_MODEL.log
|
|
+ assert xfs_info_facts.mountpoints[0].realtime == TEST_XFS_INFO_FTYPE0_MODEL.realtime
|
|
+
|
|
+
|
|
+def test_scan_xfs_no_message(monkeypatch):
|
|
+ monkeypatch.setattr(xfsinfoscanner, 'run', run_mocked())
|
|
+ monkeypatch.setattr(os.path, 'ismount', lambda _: True)
|
|
|
|
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
|
|
- assert len(api.produce.model_instances) == 1
|
|
- assert isinstance(api.produce.model_instances[0], XFSPresence)
|
|
- 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
|
|
+
|
|
+ assert api.produce.called == 2
|
|
+ assert len(api.produce.model_instances) == 2
|
|
+
|
|
+ xfs_presence = next(model for model in api.produce.model_instances if isinstance(model, XFSPresence))
|
|
+ assert not xfs_presence.present
|
|
+ assert not xfs_presence.without_ftype
|
|
+ assert not xfs_presence.mountpoints_without_ftype
|
|
+
|
|
+ xfs_info_facts = next(model for model in api.produce.model_instances if isinstance(model, XFSInfoFacts))
|
|
+ assert not xfs_info_facts.mountpoints
|
|
+
|
|
+
|
|
+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 00000000..276ca287
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/common/models/xfsinfo.py
|
|
@@ -0,0 +1,147 @@
|
|
+from leapp.models import fields, Model
|
|
+from leapp.topics import SystemInfoTopic
|
|
+
|
|
+
|
|
+class XFSInfoSection(Model):
|
|
+ """
|
|
+ Represents a section of `xfs_info`.
|
|
+ """
|
|
+
|
|
+ topic = SystemInfoTopic
|
|
+
|
|
+
|
|
+class XFSInfoMetaData(XFSInfoSection):
|
|
+ """
|
|
+ Represents the `meta-data` section of `xfs_info`.
|
|
+ """
|
|
+
|
|
+ device = fields.String()
|
|
+ bigtime = fields.Nullable(fields.String())
|
|
+ crc = fields.Nullable(fields.String())
|
|
+
|
|
+ # NOTE(dkubek): meta-data section might also contain the following fields
|
|
+ # which are not being used right now
|
|
+
|
|
+ # isize = fields.String()
|
|
+ # agcount = fields.String()
|
|
+ # agsize = fields.String()
|
|
+ # sectsz = fields.String()
|
|
+ # attr = fields.String()
|
|
+ # projid32bit = fields.String()
|
|
+ # finobt = fields.String()
|
|
+ # sparse = fields.String()
|
|
+ # rmapbt = fields.String()
|
|
+ # reflink = fields.String()
|
|
+ # inobtcount = fields.String()
|
|
+ # nrext64 = fields.String()
|
|
+
|
|
+
|
|
+class XFSInfoData(XFSInfoSection):
|
|
+ """
|
|
+ Represents the `data` section of `xfs_info`.
|
|
+ """
|
|
+
|
|
+ bsize = fields.String()
|
|
+ blocks = fields.String()
|
|
+
|
|
+ # NOTE(dkubek): data section might also contain the following fields
|
|
+ # which are not being used right now
|
|
+
|
|
+ # imaxpct = fields.String()
|
|
+ # sunit = fields.String()
|
|
+ # swidth = fields.String()
|
|
+
|
|
+
|
|
+class XFSInfoNaming(XFSInfoSection):
|
|
+ """
|
|
+ Represents the `naming` section of `xfs_info`.
|
|
+ """
|
|
+
|
|
+ ftype = fields.Nullable(fields.String())
|
|
+
|
|
+ # NOTE(dkubek): naming section might also contain the following fields
|
|
+ # which are not being used right now
|
|
+
|
|
+ # version = fields.String()
|
|
+ # bsize = fields.String()
|
|
+ # ascii_ci = fields.String()
|
|
+
|
|
+
|
|
+class XFSInfoLog(XFSInfoSection):
|
|
+ """
|
|
+ Represents the `log` section of `xfs_info`.
|
|
+ """
|
|
+
|
|
+ bsize = fields.String()
|
|
+ blocks = fields.String()
|
|
+
|
|
+ # NOTE(dkubek): log section might also contain the following fields
|
|
+ # which are not being used right now
|
|
+
|
|
+ # internal = fields.String()
|
|
+ # version = fields.String()
|
|
+ # sectsz = fields.String()
|
|
+ # sunit = fields.String()
|
|
+ # lazy_count = fields.String()
|
|
+
|
|
+
|
|
+class XFSInfoRealtime(XFSInfoSection):
|
|
+ """
|
|
+ Represents the `realtime` section of `xfs_info`.
|
|
+ """
|
|
+
|
|
+ # NOTE(dkubek): realtime section might also contain the following fields
|
|
+ # which are not being used right now
|
|
+
|
|
+ # extsz = fields.String()
|
|
+ # blocks = fields.String()
|
|
+ # rtextents = fields.String()
|
|
+
|
|
+
|
|
+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.Model(XFSInfoMetaData)
|
|
+ """
|
|
+ Attributes of 'meta-data' section.
|
|
+ """
|
|
+
|
|
+ data = fields.Model(XFSInfoData)
|
|
+ """
|
|
+ Attributes of 'data' section.
|
|
+ """
|
|
+
|
|
+ naming = fields.Model(XFSInfoNaming)
|
|
+ """
|
|
+ Attributes of 'naming' section.
|
|
+ """
|
|
+
|
|
+ log = fields.Model(XFSInfoLog)
|
|
+ """
|
|
+ Attributes of 'log' section.
|
|
+ """
|
|
+
|
|
+ realtime = fields.Model(XFSInfoRealtime)
|
|
+ """
|
|
+ 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 00000000..42974108
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/actor.py
|
|
@@ -0,0 +1,34 @@
|
|
+import leapp.libraries.actor.checkoldxfs as checkoldxfs
|
|
+from leapp.actors import Actor
|
|
+from leapp.models import XFSInfoFacts
|
|
+from leapp.reporting import Report
|
|
+from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
|
|
+
|
|
+
|
|
+class CheckOldXFS(Actor):
|
|
+ """
|
|
+ Check mounted XFS file systems.
|
|
+
|
|
+ RHEL 10 requires XFS filesystems to use the v5 format (crc=1 is a good
|
|
+ indicator). XFS v4 format filesystems will be incompatible with the target
|
|
+ kernel and it will not be possible to mount them. If any such filesystem is
|
|
+ detected, the upgrade will be inhibited.
|
|
+
|
|
+ Also, RHEL 10 is going to address the Y2K38 problem, which requires bigger
|
|
+ timestamps to support dates beyond 2038-01-19. Since RHEL 9, the "bigtime"
|
|
+ feature (indicated by bigtime=1 in xfs_info) has been introduced to resolve
|
|
+ this issue. If an XFS filesystem lacks this feature, a report will be
|
|
+ created to just raise the awareness about the potential problem to the
|
|
+ user, but the upgrade will not be blocked. This will probably be resolved
|
|
+ automatically during the RHEL 10 lifetime, it is still 10+ years in future
|
|
+ until this could have any real impact.
|
|
+
|
|
+ """
|
|
+
|
|
+ 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 00000000..0069ae7f
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/libraries/checkoldxfs.py
|
|
@@ -0,0 +1,141 @@
|
|
+from leapp import reporting
|
|
+from leapp.exceptions import StopActorExecutionError
|
|
+from leapp.libraries.stdlib import api
|
|
+from leapp.models import XFSInfoFacts
|
|
+
|
|
+RHEL_9_TO_10_BACKUP_RESTORE_LINK = 'https://red.ht/rhel_9_to_10_backup_restore_xfs'
|
|
+
|
|
+FMT_LIST_SEPARATOR = '\n - '
|
|
+
|
|
+
|
|
+def _formatted_list_output(input_list, sep=FMT_LIST_SEPARATOR):
|
|
+ return ['{}{}'.format(sep, item) for item in input_list]
|
|
+
|
|
+
|
|
+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.bigtime == '1'
|
|
+
|
|
+
|
|
+def _has_valid_crc(xfs_info):
|
|
+ return xfs_info.meta_data.crc == '1'
|
|
+
|
|
+
|
|
+def _inhibit_upgrade(invalid_bigtime, invalid_crc):
|
|
+ if invalid_bigtime:
|
|
+ _report_bigtime(invalid_bigtime)
|
|
+
|
|
+ if invalid_crc:
|
|
+ _inhibit_crc(invalid_crc)
|
|
+
|
|
+
|
|
+def _report_bigtime(invalid_bigtime):
|
|
+ title = 'Detected XFS filesystems without bigtime feature.'
|
|
+ summary = (
|
|
+ 'The XFS v5 filesystem format introduced the "bigtime" feature in RHEL 9,'
|
|
+ ' to support timestamps beyond the year 2038. XFS filesystems that'
|
|
+ ' do not have the "bigtime" feature enabled remain vulnerable to timestamp'
|
|
+ ' overflow issues. It is recommended to enable this feature on all'
|
|
+ ' XFS filesystems to ensure long-term compatibility and prevent potential'
|
|
+ ' failures.'
|
|
+ ' Following XFS file systems have not enabled the "bigtime" feature:{}'
|
|
+ .format(''.join(_formatted_list_output(invalid_bigtime)))
|
|
+ )
|
|
+
|
|
+ # NOTE(pstodulk): This will affect any system which upgraded from RHEL 8 so
|
|
+ # it is clear that such FS will have to be modified offline e.g. from
|
|
+ # initramfs - and that we speak about significant number of systems. So
|
|
+ # this should be improved yet. E.g. to update the initramfs having
|
|
+ # xfs_admin inside and working:
|
|
+ # # dracut -I "/usr/sbin/xfs_admin /usr/bin/expr" -f
|
|
+ # Note that it seems that it could be done without xfs_admin, using xfs_db
|
|
+ # only - which is present already.
|
|
+ remediation_hint = (
|
|
+ 'Enable the "bigtime" feature on XFS v5 filesystems using the command:'
|
|
+ '\n\txfs_admin -O bigtime=1 <filesystem_device>\n\n'
|
|
+ 'Note that for older XFS v5 filesystems this step can only be done'
|
|
+ ' offline right now (i.e. without the filesystem mounted).'
|
|
+ )
|
|
+
|
|
+ reporting.create_report([
|
|
+ reporting.Title(title),
|
|
+ reporting.Summary(summary),
|
|
+ reporting.Remediation(hint=remediation_hint),
|
|
+ reporting.ExternalLink(
|
|
+ title='XFS supports bigtime feature',
|
|
+ url='https://red.ht/rhel-9-xfs-bigtime',
|
|
+ ),
|
|
+ reporting.Severity(reporting.Severity.LOW),
|
|
+ ])
|
|
+
|
|
+
|
|
+def _inhibit_crc(invalid_crc):
|
|
+ title = 'Detected XFS filesystems incompatible with target kernel.'
|
|
+ summary = (
|
|
+ 'XFS v4 format has been deprecated and it has been removed from'
|
|
+ ' the target kernel. Such filesystems cannot be mounted by target'
|
|
+ ' system kernel and so the upgrade cannot proceed successfully.'
|
|
+ ' Following XFS filesystems have v4 format:{}'
|
|
+ .format(''.join(_formatted_list_output(invalid_crc)))
|
|
+ )
|
|
+ remediation_hint = (
|
|
+ 'Migrate XFS v4 filesystems to new XFS v5 format.'
|
|
+ ' For filesystems hosting data, perform a back up, reformat, and restore procedure.'
|
|
+ ' Refer to official documentation for details.'
|
|
+ ' For filesystems hosting the system a clean installation is recommended instead.'
|
|
+ )
|
|
+
|
|
+ reporting.create_report([
|
|
+ reporting.Title(title),
|
|
+ reporting.Summary(summary),
|
|
+ reporting.Remediation(hint=remediation_hint),
|
|
+ reporting.ExternalLink(
|
|
+ title='Backing up an XFS file system',
|
|
+ url='https://red.ht/rhel-9-xfs-backup',
|
|
+ ),
|
|
+ reporting.ExternalLink(
|
|
+ title='Restoring an XFS file system from backup',
|
|
+ url='https://red.ht/rhel-9-xfs-restore-from-backup',
|
|
+ ),
|
|
+ reporting.Severity(reporting.Severity.HIGH),
|
|
+ reporting.Groups([reporting.Groups.INHIBITOR]),
|
|
+ ])
|
|
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 00000000..2dd6eaa7
|
|
--- /dev/null
|
|
+++ b/repos/system_upgrade/el9toel10/actors/checkoldxfs/tests/test_checkoldxfs.py
|
|
@@ -0,0 +1,214 @@
|
|
+import pytest
|
|
+
|
|
+from leapp import reporting
|
|
+from leapp.exceptions import StopActorExecutionError
|
|
+from leapp.libraries.actor import checkoldxfs
|
|
+from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked
|
|
+from leapp.libraries.stdlib import api
|
|
+from leapp.models import (
|
|
+ XFSInfo,
|
|
+ XFSInfoData,
|
|
+ XFSInfoFacts,
|
|
+ XFSInfoLog,
|
|
+ XFSInfoMetaData,
|
|
+ XFSInfoNaming,
|
|
+ XFSInfoRealtime
|
|
+)
|
|
+from leapp.utils.report import is_inhibitor
|
|
+
|
|
+
|
|
+def test_has_valid_bigtime_passes():
|
|
+ """
|
|
+ Test _has_valid_bigtime passes for correct attributes.
|
|
+ """
|
|
+
|
|
+ xfs_info = XFSInfo(
|
|
+ mountpoint='/MOUNTPOINT',
|
|
+ meta_data=XFSInfoMetaData(bigtime='1', crc=None, device='/dev/vda'),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime(),
|
|
+ )
|
|
+
|
|
+ assert checkoldxfs._has_valid_bigtime(xfs_info)
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize("bigtime", ['0', '', '<UNKNOWN>', None])
|
|
+def test_has_valid_bigtime_fail(bigtime):
|
|
+ """
|
|
+ Test _has_valid_bigtime fails for incorrect attributes.
|
|
+ """
|
|
+
|
|
+ xfs_info = XFSInfo(
|
|
+ mountpoint='/MOUNTPOINT',
|
|
+ meta_data=(
|
|
+ XFSInfoMetaData(bigtime=bigtime, crc=None, device='/dev/vda')
|
|
+ if bigtime
|
|
+ else XFSInfoMetaData(device='/dev/vda')
|
|
+ ),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime(),
|
|
+ )
|
|
+
|
|
+ assert not checkoldxfs._has_valid_bigtime(xfs_info)
|
|
+
|
|
+
|
|
+def test_has_valid_crc_passes():
|
|
+ """
|
|
+ Test _has_valid_crc passes for correct attributes.
|
|
+ """
|
|
+
|
|
+ xfs_info = XFSInfo(
|
|
+ mountpoint='/MOUNTPOINT',
|
|
+ meta_data=XFSInfoMetaData(crc='1', bigtime=None, device='/dev/vda'),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime(),
|
|
+ )
|
|
+
|
|
+ assert checkoldxfs._has_valid_crc(xfs_info)
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize("crc", ['0', '', '<UNKNOWN>', None])
|
|
+def test_has_valid_crc_fail(crc):
|
|
+ """
|
|
+ Test _has_valid_crc fails for incorrect attributes.
|
|
+ """
|
|
+
|
|
+ xfs_info = XFSInfo(
|
|
+ mountpoint='/MOUNTPOINT',
|
|
+ meta_data=(
|
|
+ XFSInfoMetaData(crc=crc, bigtime=None, device='/dev/vda')
|
|
+ if crc
|
|
+ else XFSInfoMetaData(device='/dev/vda')
|
|
+ ),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime(),
|
|
+ )
|
|
+
|
|
+ assert not checkoldxfs._has_valid_crc(xfs_info)
|
|
+
|
|
+
|
|
+def test_get_xfs_info_facts_info_single_entry(monkeypatch):
|
|
+ xfs_info_facts = XFSInfoFacts(mountpoints=[])
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[xfs_info_facts]))
|
|
+
|
|
+ result = checkoldxfs._get_xfs_info_facts()
|
|
+ assert result == xfs_info_facts
|
|
+
|
|
+
|
|
+def test_get_workaround_efi_info_multiple_entries(monkeypatch):
|
|
+ logger = logger_mocked()
|
|
+ xfs_info_facts = XFSInfoFacts(mountpoints=[])
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
|
|
+ msgs=[xfs_info_facts, xfs_info_facts]))
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+
|
|
+ result = checkoldxfs._get_xfs_info_facts()
|
|
+ assert result == xfs_info_facts
|
|
+ assert 'Unexpectedly received more than one XFSInfoFacts message.' in logger.warnmsg
|
|
+
|
|
+
|
|
+def test_get_workaround_efi_info_no_entry(monkeypatch):
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[]))
|
|
+
|
|
+ with pytest.raises(StopActorExecutionError, match='Could not retrieve XFSInfoFacts!'):
|
|
+ checkoldxfs._get_xfs_info_facts()
|
|
+
|
|
+
|
|
+def test_valid_xfs_passes(monkeypatch):
|
|
+ """
|
|
+ Test no report is generated for valid XFS mountpoint
|
|
+ """
|
|
+
|
|
+ logger = logger_mocked()
|
|
+ monkeypatch.setattr(reporting, "create_report", create_report_mocked())
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[
|
|
+ XFSInfoFacts(
|
|
+ mountpoints=[
|
|
+ XFSInfo(
|
|
+ mountpoint='/MOUNTPOINT',
|
|
+ meta_data=XFSInfoMetaData(crc='1', bigtime='1', device='/dev/vda'),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime(),
|
|
+ ),
|
|
+ ]
|
|
+ )
|
|
+ ]))
|
|
+
|
|
+ checkoldxfs.process()
|
|
+
|
|
+ assert 'All XFS system detected are valid.' in logger.dbgmsg[0]
|
|
+ assert not reporting.create_report.called
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize(
|
|
+ 'valid_crc,valid_bigtime',
|
|
+ [
|
|
+ (False, True),
|
|
+ (True, False),
|
|
+ (False, False),
|
|
+ ]
|
|
+)
|
|
+def test_unsupported_xfs(monkeypatch, valid_crc, valid_bigtime):
|
|
+ """
|
|
+ Test report is generated for unsupported XFS mountpoint
|
|
+ """
|
|
+
|
|
+ logger = logger_mocked()
|
|
+ monkeypatch.setattr(reporting, "create_report", create_report_mocked())
|
|
+ monkeypatch.setattr(api, 'current_logger', logger)
|
|
+ monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[
|
|
+ XFSInfoFacts(
|
|
+ mountpoints=[
|
|
+ XFSInfo(
|
|
+ mountpoint='/MOUNTPOINT',
|
|
+ meta_data=XFSInfoMetaData(
|
|
+ crc='1' if valid_crc else '0',
|
|
+ bigtime='1' if valid_bigtime else '0',
|
|
+ device='/dev/vda',
|
|
+ ),
|
|
+ data=XFSInfoData(blocks='524288', bsize='4096'),
|
|
+ naming=XFSInfoNaming(),
|
|
+ log=XFSInfoLog(blocks='2560', bsize='4096'),
|
|
+ realtime=XFSInfoRealtime(),
|
|
+ ),
|
|
+ ]
|
|
+ )
|
|
+ ]))
|
|
+
|
|
+ checkoldxfs.process()
|
|
+
|
|
+ assert reporting.create_report.called == (int(not valid_crc) + int(not valid_bigtime))
|
|
+
|
|
+ if not valid_crc:
|
|
+ reports = [
|
|
+ report
|
|
+ for report in reporting.create_report.reports
|
|
+ if report.get('title') == 'Detected XFS filesystems incompatible with target kernel.'
|
|
+ ]
|
|
+ assert reports
|
|
+ report = reports[-1]
|
|
+ assert 'XFS v4 format has been deprecated' in report.get('summary')
|
|
+ assert report['severity'] == reporting.Severity.HIGH
|
|
+ assert is_inhibitor(report)
|
|
+
|
|
+ if not valid_bigtime:
|
|
+ reports = [
|
|
+ report
|
|
+ for report in reporting.create_report.reports
|
|
+ if report.get('title') == 'Detected XFS filesystems without bigtime feature.'
|
|
+ ]
|
|
+ assert reports
|
|
+ report = reports[-1]
|
|
+ assert 'XFS v5 filesystem format introduced the "bigtime" feature' in report.get('summary')
|
|
+ assert report['severity'] == reporting.Severity.LOW
|
|
--
|
|
2.48.1
|
|
|