Skip to content

Commit 190f543

Browse files
committed
add handling for LVM configuration
The relevant user LVM configuration is now copied into the target userspace container along with enabling LVM dracut module for upgrade initramfs creation. The LVM configuration is copied into the target userspace container when lvm2 package in installed. Based on the configuration, the devices file is also copied in when present and enabled. The --nolvmconf option used when executing dracut is changed into --lvmconf instead if the files are copied into the target userspace container. Jira: RHEL-14712
1 parent eabab8c commit 190f543

File tree

9 files changed

+469
-13
lines changed

9 files changed

+469
-13
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor.checklvm import check_lvm
3+
from leapp.models import DistributionSignedRPM, LVMConfig, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks
4+
from leapp.reporting import Report
5+
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
6+
7+
8+
class CheckLVM(Actor):
9+
"""
10+
Check if the LVM is installed and ensure the target userspace container
11+
and initramfs are prepared to support it.
12+
13+
The LVM configuration files are copied into the target userspace container
14+
so that the dracut is able to use them while creating the initramfs.
15+
The dracut LVM module is enabled by this actor as well.
16+
"""
17+
18+
name = 'check_lvm'
19+
consumes = (DistributionSignedRPM, LVMConfig)
20+
produces = (Report, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks)
21+
tags = (ChecksPhaseTag, IPUWorkflowTag)
22+
23+
def process(self):
24+
check_lvm()
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import os
2+
3+
from leapp import reporting
4+
from leapp.libraries.common.rpms import has_package
5+
from leapp.libraries.stdlib import api
6+
from leapp.models import (
7+
CopyFile,
8+
DistributionSignedRPM,
9+
DracutModule,
10+
LVMConfig,
11+
TargetUserSpaceUpgradeTasks,
12+
UpgradeInitramfsTasks
13+
)
14+
15+
LVM_CONFIG_PATH = '/etc/lvm/lvm.conf'
16+
LVM_DEVICES_FILE_PATH_PREFIX = '/etc/lvm/devices'
17+
18+
19+
def _report_filter_detection():
20+
title = 'LVM filter definition detected.'
21+
summary = (
22+
'RHEL 9 and above uses the LVM devices file by default to select devices used by LVM. '
23+
f'Since this system has LVM filter defined in the {LVM_CONFIG_PATH}, it will be '
24+
'used after the upgrade as well.'
25+
)
26+
27+
remediation_hint = (
28+
'While not mandatory, switching to the LVM devices file from the LVM filter is possible '
29+
'using the following command. It uses the existing LVM filter to create the system.devices '
30+
'file which is then used instead of the LVM filter as long as it exists. Before running the command, '
31+
'make sure that the use_devicesfile=1 (default for RHEL 9 and above).'
32+
)
33+
remediation_command = ['vgimportdevices', '-a']
34+
35+
reporting.create_report([
36+
reporting.Title(title),
37+
reporting.Summary(summary),
38+
reporting.Remediation(hint=remediation_hint, commands=[remediation_command]),
39+
reporting.ExternalLink(
40+
title='Limiting LVM device visibility and usage',
41+
url='https://red.ht/3MfgK7c',
42+
),
43+
reporting.Severity(reporting.Severity.INFO),
44+
])
45+
46+
47+
def check_lvm():
48+
if not has_package(DistributionSignedRPM, 'lvm2'):
49+
return
50+
51+
lvm_config = next(api.consume(LVMConfig), None)
52+
if not lvm_config:
53+
return
54+
55+
lvm_devices_file_path = os.path.join(LVM_DEVICES_FILE_PATH_PREFIX, lvm_config.devices.devicesfile)
56+
lvm_devices_file_exists = os.path.isfile(lvm_devices_file_path)
57+
58+
filters_used = not lvm_config.devices.use_devicesfile or not lvm_devices_file_exists
59+
if filters_used:
60+
_report_filter_detection()
61+
62+
api.current_logger().debug('Including lvm dracut module.')
63+
api.produce(UpgradeInitramfsTasks(include_dracut_modules=[DracutModule(name='lvm')]))
64+
# TODO: decide if we need to install lvm2 package in the container as well
65+
66+
copy_files = []
67+
api.current_logger().debug('Copying "{}" to the target userspace.'.format(LVM_CONFIG_PATH))
68+
copy_files.append(CopyFile(src=LVM_CONFIG_PATH))
69+
70+
if lvm_devices_file_exists and lvm_config.devices.use_devicesfile:
71+
api.current_logger().debug('Copying "{}" to the target userspace.'.format(lvm_devices_file_path))
72+
copy_files.append(CopyFile(src=lvm_devices_file_path))
73+
74+
api.produce(TargetUserSpaceUpgradeTasks(copy_files=copy_files))
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import os
2+
3+
import pytest
4+
5+
from leapp.libraries.actor import checklvm
6+
from leapp.libraries.common.testutils import produce_mocked
7+
from leapp.libraries.stdlib import api
8+
from leapp.models import LVMConfig, LVMConfigDevicesSection, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks
9+
10+
11+
def test_check_lvm_when_lvm_not_installed(monkeypatch):
12+
def has_package_mocked(_, package_name):
13+
assert package_name == 'lvm2'
14+
return False
15+
16+
def consume_mocked(_):
17+
assert False
18+
19+
monkeypatch.setattr(checklvm, 'has_package', has_package_mocked)
20+
monkeypatch.setattr(api, 'produce', produce_mocked())
21+
monkeypatch.setattr(api, 'consume', consume_mocked)
22+
23+
checklvm.check_lvm()
24+
25+
assert not api.produce.called
26+
27+
28+
@pytest.mark.parametrize(
29+
('config', 'create_report', 'devices_file_exists'),
30+
[
31+
(LVMConfig(devices=LVMConfigDevicesSection(use_devicesfile=False)), True, False),
32+
(LVMConfig(devices=LVMConfigDevicesSection(use_devicesfile=True)), False, True),
33+
(LVMConfig(devices=LVMConfigDevicesSection(use_devicesfile=True)), True, False),
34+
(LVMConfig(devices=LVMConfigDevicesSection(use_devicesfile=False, devicesfile="test.devices")), True, False),
35+
(LVMConfig(devices=LVMConfigDevicesSection(use_devicesfile=True, devicesfile="test.devices")), False, True),
36+
(LVMConfig(devices=LVMConfigDevicesSection(use_devicesfile=True, devicesfile="test.devices")), True, False),
37+
]
38+
)
39+
def test_scan_when_lvm_installed(monkeypatch, config, create_report, devices_file_exists):
40+
41+
def has_package_mocked(_, package_name):
42+
assert package_name == 'lvm2'
43+
return True
44+
45+
def isfile_mocked(_):
46+
return devices_file_exists
47+
48+
def consume_mocked(model):
49+
if model == LVMConfig:
50+
yield config
51+
52+
def report_filter_detection_mocked():
53+
assert create_report
54+
55+
monkeypatch.setattr(checklvm, 'has_package', has_package_mocked)
56+
monkeypatch.setattr(api, 'produce', produce_mocked())
57+
monkeypatch.setattr(api, 'consume', consume_mocked)
58+
monkeypatch.setattr(os.path, 'isfile', isfile_mocked)
59+
monkeypatch.setattr(checklvm, '_report_filter_detection', report_filter_detection_mocked)
60+
61+
checklvm.check_lvm()
62+
63+
# The lvm is installed, thus the dracut module is enabled and at least the lvm.conf is coppied
64+
assert api.produce.called == 2
65+
assert len(api.produce.model_instances) == 2
66+
67+
expected_copied_files = [checklvm.LVM_CONFIG_PATH]
68+
if devices_file_exists and config.devices.use_devicesfile:
69+
devices_file_path = os.path.join(checklvm.LVM_DEVICES_FILE_PATH_PREFIX, config.devices.devicesfile)
70+
expected_copied_files.append(devices_file_path)
71+
72+
for produced_model in api.produce.model_instances:
73+
assert isinstance(produced_model, (UpgradeInitramfsTasks, TargetUserSpaceUpgradeTasks))
74+
75+
if isinstance(produced_model, UpgradeInitramfsTasks):
76+
assert len(produced_model.include_dracut_modules) == 1
77+
assert produced_model.include_dracut_modules[0].name == 'lvm'
78+
else:
79+
assert len(produced_model.copy_files) == len(expected_copied_files)
80+
for file in produced_model.copy_files:
81+
assert file.src in expected_copied_files

repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
BootContent,
77
FIPSInfo,
88
LiveModeConfig,
9+
LVMConfig,
910
TargetOSInstallationImage,
1011
TargetUserSpaceInfo,
1112
TargetUserSpaceUpgradeTasks,
@@ -31,6 +32,7 @@ class UpgradeInitramfsGenerator(Actor):
3132
consumes = (
3233
FIPSInfo,
3334
LiveModeConfig,
35+
LVMConfig,
3436
RequiredUpgradeInitramPackages, # deprecated
3537
TargetOSInstallationImage,
3638
TargetUserSpaceInfo,

repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/libraries/upgradeinitramfsgenerator.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from leapp.models import (
1313
BootContent,
1414
LiveModeConfig,
15+
LVMConfig,
1516
TargetOSInstallationImage,
1617
TargetUserSpaceInfo,
1718
TargetUserSpaceUpgradeTasks,
@@ -363,20 +364,29 @@ def generate_initram_disk(context):
363364
def fmt_module_list(module_list):
364365
return ','.join(mod.name for mod in module_list)
365366

367+
env_variables = [
368+
'LEAPP_KERNEL_VERSION={kernel_version}',
369+
'LEAPP_ADD_DRACUT_MODULES="{dracut_modules}"',
370+
'LEAPP_KERNEL_ARCH={arch}',
371+
'LEAPP_ADD_KERNEL_MODULES="{kernel_modules}"',
372+
'LEAPP_DRACUT_INSTALL_FILES="{files}"'
373+
]
374+
375+
if next(api.consume(LVMConfig), None):
376+
env_variables.append('LEAPP_DRACUT_LVMCONF="1"')
377+
378+
env_variables = ' '.join(env_variables)
379+
env_variables = env_variables.format(
380+
kernel_version=_get_target_kernel_version(context),
381+
dracut_modules=fmt_module_list(initramfs_includes.dracut_modules),
382+
kernel_modules=fmt_module_list(initramfs_includes.kernel_modules),
383+
arch=api.current_actor().configuration.architecture,
384+
files=' '.join(initramfs_includes.files)
385+
)
386+
cmd = os.path.join('/', INITRAM_GEN_SCRIPT_NAME)
387+
366388
# FIXME: issue #376
367-
context.call([
368-
'/bin/sh', '-c',
369-
'LEAPP_KERNEL_VERSION={kernel_version} '
370-
'LEAPP_ADD_DRACUT_MODULES="{dracut_modules}" LEAPP_KERNEL_ARCH={arch} '
371-
'LEAPP_ADD_KERNEL_MODULES="{kernel_modules}" '
372-
'LEAPP_DRACUT_INSTALL_FILES="{files}" {cmd}'.format(
373-
kernel_version=_get_target_kernel_version(context),
374-
dracut_modules=fmt_module_list(initramfs_includes.dracut_modules),
375-
kernel_modules=fmt_module_list(initramfs_includes.kernel_modules),
376-
arch=api.current_actor().configuration.architecture,
377-
files=' '.join(initramfs_includes.files),
378-
cmd=os.path.join('/', INITRAM_GEN_SCRIPT_NAME))
379-
], env=env)
389+
context.call(['/bin/sh', '-c', f'{env_variables} {cmd}'], env=env)
380390

381391
boot_files_info = copy_boot_files(context)
382392
return boot_files_info
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor import scanner
3+
from leapp.models import DistributionSignedRPM, LVMConfig
4+
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
5+
6+
7+
class ScanLVMConfig(Actor):
8+
"""
9+
Scan LVM configuration.
10+
"""
11+
12+
name = 'scan_lvm_config'
13+
consumes = (DistributionSignedRPM,)
14+
produces = (LVMConfig,)
15+
tags = (FactsPhaseTag, IPUWorkflowTag)
16+
17+
def process(self):
18+
scanner.scan()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import os
2+
import re
3+
4+
from leapp.libraries.common.config.version import get_source_major_version
5+
from leapp.libraries.common.rpms import has_package
6+
from leapp.libraries.stdlib import api
7+
from leapp.models import DistributionSignedRPM, LVMConfig, LVMConfigDevicesSection
8+
9+
LVM_CONFIG_PATH = '/etc/lvm/lvm.conf'
10+
11+
12+
def _lvm_config_devices_parser(lvm_config_lines):
13+
in_section = False
14+
config = {}
15+
for line in lvm_config_lines:
16+
line = line.split("#", 1)[0].strip()
17+
if not line:
18+
continue
19+
if "devices {" in line:
20+
in_section = True
21+
continue
22+
if in_section and "}" in line:
23+
in_section = False
24+
if in_section:
25+
value = line.split("=", 1)
26+
config[value[0].strip()] = value[1].strip().strip('"')
27+
return config
28+
29+
30+
def _read_config_lines(path):
31+
with open(path) as lvm_conf_file:
32+
return lvm_conf_file.readlines()
33+
34+
35+
def scan():
36+
if not has_package(DistributionSignedRPM, 'lvm2'):
37+
return
38+
39+
if not os.path.isfile(LVM_CONFIG_PATH):
40+
api.current_logger().debug('The "{}" is not present on the system.'.format(LVM_CONFIG_PATH))
41+
return
42+
43+
lvm_config_lines = _read_config_lines(LVM_CONFIG_PATH)
44+
devices_section = _lvm_config_devices_parser(lvm_config_lines)
45+
46+
lvm_config_devices = LVMConfigDevicesSection(use_devicesfile=int(get_source_major_version()) > 8)
47+
if 'devicesfile' in devices_section:
48+
lvm_config_devices.devicesfile = devices_section['devicesfile']
49+
50+
if 'use_devicesfile' in devices_section and devices_section['use_devicesfile'] in ['0', '1']:
51+
lvm_config_devices.use_devicesfile = devices_section['use_devicesfile'] == '1'
52+
53+
api.produce(LVMConfig(devices=lvm_config_devices))

0 commit comments

Comments
 (0)