Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inhibit upgrades with unsupported XFS #1318

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading