Skip to content

Commit

Permalink
Add inhibitor for unsupported XFS
Browse files Browse the repository at this point in the history
RHEL 10 introduces stricter requirements for XFS filesystems. If any XFS
filesystem on the system lack these required features, the upgrade will
be inhibited.

JIRA: RHEL-60034
  • Loading branch information
dkubek committed Dec 16, 2024
1 parent 3c3421a commit 2bedbf8
Show file tree
Hide file tree
Showing 7 changed files with 795 additions and 159 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,7 +1,66 @@
import os
import re

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import StorageInfo, XFSPresence
from leapp.models import StorageInfo, XFSInfo, XFSInfoFacts, 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=[
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):
Expand All @@ -22,43 +81,97 @@ def scan_xfs_mount(data):
return mountpoints


def is_xfs_without_ftype(mp):
def read_xfs_info(mp):
if not is_mountpoint(mp):
return None

try:
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 None

return result['stdout']


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
try:
xfs_info = 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 True

return False

def parse_xfs_info(content):
"""
This parser reads the output of the ``xfs_info`` command.
def scan_xfs():
storage_info_msgs = api.consume(StorageInfo)
storage_info = next(storage_info_msgs, None)
In general the pattern is::
if list(storage_info_msgs):
api.current_logger().warning('Unexpectedly received more than one StorageInfo message.')
section =sectionkey key1=value1 key2=value2, key3=value3
= key4=value4
nextsec =sectionkey sectionvalue key=value otherkey=othervalue
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)
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.
mountpoints = fstab_data | mount_data
mountpoints_ftype0 = list(filter(is_xfs_without_ftype, mountpoints))
NOTE: This function is adapted from [1]
# 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,
))
[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

_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'
Loading

0 comments on commit 2bedbf8

Please sign in to comment.