Skip to content

Commit

Permalink
Add inhibitor for unsupported XFS
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dkubek authored and pirat89 committed Jan 29, 2025
1 parent 0a5f66e commit c5accf4
Show file tree
Hide file tree
Showing 7 changed files with 1,027 additions and 167 deletions.
24 changes: 17 additions & 7 deletions repos/system_upgrade/common/actors/xfsinfoscanner/actor.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Loading

0 comments on commit c5accf4

Please sign in to comment.