Skip to content

Commit

Permalink
feature: add possibility to use net.naming-scheme
Browse files Browse the repository at this point in the history
Leapp writes .link files to prevent interfaces being renamed
after booting to post-upgrade system. This patch adds a less
error-prone approach that uses net.naming-scheme kernel param.
The naming-scheme tells udev what hardware properties to use
when composing a device name. Moreover, possible values of this
parameter are coarse-grained "profiles", that tell udev to
behave as if it did on RHEL8.0.

The functionality is enabled by setting LEAPP_USE_NET_NAMING_SCHEME
environmental variable to 1. If the feature is enabled, the .link
file generation is disabled. A kernel parameter `net.naming-scheme=`
is added to the upgrade boot entry and the post-upgrade entry.
The value of the parameter will be `rhel-<source_major>.0`. Note
that the minor source version is *not used*. Using also source major
version instead of 0 causes the device names to change slightly,
so we use 0. Moreover, an extra RPM named `rhel-net-naming-sysattrs`
is installed to the target system and target userspace container.
The RPM provides definitions of the "profiles" for net.naming-scheme.

The feature is available only for 8>9 and higher. Attempting to
upgrade 7>8 with LEAPP_USE_NET_NAMING_SCHEME=1 will ignore
the value of LEAPP_USE_NET_NAMING_SCHEME.

Add a possibility to use the net.naming-scheme cmdline argument
to make immutable network interface names during the upgrade.
The feature can be used only for 8>9 upgrades and higher.
To enable the feature, use LEAPP_USE_NET_NAMING_SCHEME=1.

Jira-ref: RHEL-23473
  • Loading branch information
mhecko committed Oct 22, 2024
1 parent c2c96af commit 6a32159
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 57 deletions.
10 changes: 7 additions & 3 deletions repos/system_upgrade/common/actors/addupgradebootentry/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
FirmwareFacts,
GrubConfigError,
KernelCmdline,
LateTargetKernelCmdlineArgTasks,
LiveImagePreparationInfo,
LiveModeArtifacts,
LiveModeConfig,
TargetKernelCmdlineArgTasks,
TransactionDryRun
TransactionDryRun,
UpgradeKernelCmdlineArgTasks
)
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag

Expand All @@ -33,9 +35,11 @@ class AddUpgradeBootEntry(Actor):
LiveModeArtifacts,
LiveModeConfig,
KernelCmdline,
TransactionDryRun
TransactionDryRun,
TargetKernelCmdlineArgTasks,
UpgradeKernelCmdlineArgTasks
)
produces = (TargetKernelCmdlineArgTasks,)
produces = (LateTargetKernelCmdlineArgTasks,)
tags = (IPUWorkflowTag, InterimPreparationPhaseTag)

def process(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
BootContent,
KernelCmdline,
KernelCmdlineArg,
LateTargetKernelCmdlineArgTasks,
LiveImagePreparationInfo,
LiveModeArtifacts,
LiveModeConfig,
TargetKernelCmdlineArgTasks
TargetKernelCmdlineArgTasks,
UpgradeKernelCmdlineArgTasks
)


def collect_boot_args(livemode_enabled):
def collect_upgrade_kernel_args(livemode_enabled):
args = {
'enforcing': '0',
'rd.plymouth': '0',
Expand All @@ -34,7 +36,10 @@ def collect_boot_args(livemode_enabled):
livemode_args = construct_cmdline_args_for_livemode()
args.update(livemode_args)

return args
upgrade_kernel_args = collect_set_of_kernel_args_from_msgs(UpgradeKernelCmdlineArgTasks, 'to_add')
args.update(upgrade_kernel_args)

return set(args.items())


def collect_undesired_args(livemode_enabled):
Expand All @@ -43,11 +48,11 @@ def collect_undesired_args(livemode_enabled):
args = dict(zip(('ro', 'rhgb', 'quiet'), itertools.repeat(None)))
args['rd.lvm.lv'] = _get_rdlvm_arg_values()

return args
return set(args.items())


def format_grubby_args_from_args_dict(args_dict):
""" Format the given args dictionary in a form required by grubby's --args. """
def format_grubby_args_from_args_set(args_dict):
""" Format the given args set in a form required by grubby's --args. """

def fmt_single_arg(arg_pair):
key, value = arg_pair
Expand All @@ -65,7 +70,7 @@ def flatten_arguments(arg_pair):
else:
yield (key, value) # Just a single (key, value) pair

arg_sequence = itertools.chain(*(flatten_arguments(arg_pair) for arg_pair in args_dict.items()))
arg_sequence = itertools.chain(*(flatten_arguments(arg_pair) for arg_pair in args_dict))

# Sorting should be fine as only values can be None, but we cannot have a (key, None) and (key, value) in
# the dictionary at the same time.
Expand All @@ -78,7 +83,7 @@ def flatten_arguments(arg_pair):
def figure_out_commands_needed_to_add_entry(kernel_path, initramfs_path, args_to_add, args_to_remove):
boot_entry_modification_commands = []

args_to_add_str = format_grubby_args_from_args_dict(args_to_add)
args_to_add_str = format_grubby_args_from_args_set(args_to_add)

create_entry_cmd = [
'/usr/sbin/grubby',
Expand All @@ -93,7 +98,7 @@ def figure_out_commands_needed_to_add_entry(kernel_path, initramfs_path, args_to

# We need to update root= param separately, since we cannot do it during --add-kernel with --copy-default.
# This is likely a bug in grubby.
root_param_value = args_to_add.get('root', None)
root_param_value = dict(args_to_add).get('root', None)
if root_param_value:
enforce_root_param_for_the_entry_cmd = [
'/usr/sbin/grubby',
Expand All @@ -103,7 +108,7 @@ def figure_out_commands_needed_to_add_entry(kernel_path, initramfs_path, args_to
boot_entry_modification_commands.append(enforce_root_param_for_the_entry_cmd)

if args_to_remove:
args_to_remove_str = format_grubby_args_from_args_dict(args_to_remove)
args_to_remove_str = format_grubby_args_from_args_set(args_to_remove)
remove_undesired_args_cmd = [
'/usr/sbin/grubby',
'--update-kernel', kernel_path,
Expand All @@ -113,18 +118,55 @@ def figure_out_commands_needed_to_add_entry(kernel_path, initramfs_path, args_to
return boot_entry_modification_commands


def collect_set_of_kernel_args_from_msgs(msg_type, arg_list_field_name):
cmdline_modification_msgs = api.consume(msg_type)
lists_of_args_to_add = (getattr(msg, arg_list_field_name, []) for msg in cmdline_modification_msgs)
args = itertools.chain(*lists_of_args_to_add)
return set((arg.key, arg.value) for arg in args)


def emit_removal_of_args_meant_only_for_upgrade_kernel(added_upgrade_kernel_args):
"""
Emit message requesting removal of upgrade kernel args that should not be on the target kernel.
Target kernel args are created by copying the args of the booted (upgrade) kernel. Therefore,
we need to explicitly modify the target kernel cmdline, removing what should not have been copied.
"""
target_args_to_add = collect_set_of_kernel_args_from_msgs(TargetKernelCmdlineArgTasks, 'to_add')
actual_kernel_args = collect_set_of_kernel_args_from_msgs(KernelCmdline, 'parameters')

# actual_kernel_args should not be changed during upgrade, unless explicitly removed by
# TargetKernelCmdlineArgTasks.to_remove, but that is handled by some other upgrade component. We just want
# to make sure we remove what was not on the source system and that we don't overwrite args to be added to target.
args_not_present_on_target_kernel = added_upgrade_kernel_args - actual_kernel_args - target_args_to_add

# We remove only what we've added and what will not be already removed by someone else.
args_to_remove = [KernelCmdlineArg(key=arg[0], value=arg[1]) for arg in args_not_present_on_target_kernel]

if args_to_remove:
msg = ('Following upgrade kernel args were added, but they should not be present '
'on target cmdline: `%s`, requesting removal.')
api.current_logger().info(msg, args_not_present_on_target_kernel)
args_sorted = sorted(args_to_remove, key=lambda arg: arg.key)
api.produce(LateTargetKernelCmdlineArgTasks(to_remove=args_sorted))


def add_boot_entry(configs=None):
kernel_dst_path, initram_dst_path = get_boot_file_paths()

_remove_old_upgrade_boot_entry(kernel_dst_path, configs=configs)

livemode_enabled = next(api.consume(LiveImagePreparationInfo), None) is not None

cmdline_args = collect_boot_args(livemode_enabled)
# We have to keep the desired and unwanted args separate and modify cmline in two separate grubby calls. Merging
# these sets and trying to execute only a single command would leave the unwanted cmdline args present if they
# are present on the original system.
added_cmdline_args = collect_upgrade_kernel_args(livemode_enabled)
undesired_cmdline_args = collect_undesired_args(livemode_enabled)

commands_to_run = figure_out_commands_needed_to_add_entry(kernel_dst_path,
initram_dst_path,
args_to_add=cmdline_args,
args_to_add=added_cmdline_args,
args_to_remove=undesired_cmdline_args)

def run_commands_adding_entry(extra_command_suffix=None):
Expand All @@ -146,16 +188,8 @@ def run_commands_adding_entry(extra_command_suffix=None):
# See https://bugzilla.redhat.com/show_bug.cgi?id=1764306
run(['/usr/sbin/zipl'])

if 'debug' in cmdline_args:
# The kernelopts for target kernel are generated based on the cmdline used in the upgrade initramfs,
# therefore, if we enabled debug above, and the original system did not have the debug kernelopt, we
# need to explicitly remove it from the target os boot entry.
# NOTE(mhecko): This will also unconditionally remove debug kernelopt if the source system used it.
api.produce(TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='debug')]))

# NOTE(mmatuska): This will remove the option even if the source system had it set.
# However enforcing=0 shouldn't be set persistently anyway.
api.produce(TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='enforcing', value='0')]))
effective_upgrade_kernel_args = added_cmdline_args - undesired_cmdline_args
emit_removal_of_args_meant_only_for_upgrade_kernel(effective_upgrade_kernel_args)

except CalledProcessError as e:
raise StopActorExecutionError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BootContent,
KernelCmdline,
KernelCmdlineArg,
LateTargetKernelCmdlineArgTasks,
LiveModeArtifacts,
LiveModeConfig,
TargetKernelCmdlineArgTasks
Expand Down Expand Up @@ -82,8 +83,10 @@ def get_boot_file_paths_mocked():
assert addupgradebootentry.run.args[0] == run_args.args_remove
assert addupgradebootentry.run.args[1] == run_args.args_add
assert api.produce.model_instances == [
TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='debug')]),
TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='enforcing', value='0')])
LateTargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='debug'),
KernelCmdlineArg(key='enforcing', value='0'),
KernelCmdlineArg(key='plymouth.enable', value='0'),
KernelCmdlineArg(key='rd.plymouth', value='0')])
]

if run_args.args_zipl:
Expand All @@ -103,16 +106,16 @@ def get_boot_file_paths_mocked():
CurrentActorMocked(envars={'LEAPP_DEBUG': str(int(is_leapp_invoked_with_debug))}))

addupgradebootentry.add_boot_entry()
assert len(api.produce.model_instances) == 1

expected_produced_messages = []
if is_leapp_invoked_with_debug:
expected_produced_messages = [TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='debug')])]

expected_produced_messages.append(
TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='enforcing', value='0')])
)
produced_msg = api.produce.model_instances[0]
assert isinstance(produced_msg, LateTargetKernelCmdlineArgTasks)

assert api.produce.model_instances == expected_produced_messages
debug_kernel_cmline_arg = KernelCmdlineArg(key='debug')
if is_leapp_invoked_with_debug:
assert debug_kernel_cmline_arg in produced_msg.to_remove
else:
assert debug_kernel_cmline_arg not in produced_msg.to_remove


def test_add_boot_entry_configs(monkeypatch):
Expand All @@ -132,8 +135,10 @@ def get_boot_file_paths_mocked():
assert addupgradebootentry.run.args[2] == run_args_add + ['-c', CONFIGS[0]]
assert addupgradebootentry.run.args[3] == run_args_add + ['-c', CONFIGS[1]]
assert api.produce.model_instances == [
TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='debug')]),
TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='enforcing', value='0')]),
LateTargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='debug'),
KernelCmdlineArg(key='enforcing', value='0'),
KernelCmdlineArg(key='plymouth.enable', value='0'),
KernelCmdlineArg(key='rd.plymouth', value='0')])
]


Expand Down Expand Up @@ -183,7 +188,7 @@ def test_fix_grub_config_error(monkeypatch, error_type, test_file_name):
(False, False),
)
)
def test_collect_boot_args(monkeypatch, is_debug_enabled, network_enablement_type):
def test_collect_upgrade_kernel_args(monkeypatch, is_debug_enabled, network_enablement_type):
env_vars = {'LEAPP_DEBUG': str(int(is_debug_enabled))}
if network_enablement_type:
env_vars['LEAPP_DEVEL_INITRAM_NETWORK'] = network_enablement_type
Expand All @@ -192,7 +197,8 @@ def test_collect_boot_args(monkeypatch, is_debug_enabled, network_enablement_typ
monkeypatch.setattr(addupgradebootentry, 'construct_cmdline_args_for_livemode',
lambda *args: {'livemodearg': 'value'})

args = addupgradebootentry.collect_boot_args(livemode_enabled=True)
arg_set = addupgradebootentry.collect_upgrade_kernel_args(livemode_enabled=True)
args = dict(arg_set)

assert args['enforcing'] == '0'
assert args['rd.plymouth'] == '0'
Expand Down Expand Up @@ -320,16 +326,3 @@ def readlink_mock(path):
uuid = addupgradebootentry._get_device_uuid(path)

assert uuid == 'MY_UUID1'


@pytest.mark.parametrize(
('args', 'expected_result'),
(
([('argA', 'val'), ('argB', 'valB'), ('argC', None), ], 'argA=val argB=valB argC'),
([('argA', ('val1', 'val2'))], 'argA=val1 argA=val2')
)
)
def test_format_grubby_args_from_args_dict(args, expected_result):
actual_result = addupgradebootentry.format_grubby_args_from_args_dict(dict(args))

assert actual_result == expected_result
16 changes: 14 additions & 2 deletions repos/system_upgrade/common/actors/kernelcmdlineconfig/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import kernelcmdlineconfig
from leapp.models import FirmwareFacts, InstalledTargetKernelInfo, KernelCmdlineArg, TargetKernelCmdlineArgTasks
from leapp.models import (
FirmwareFacts,
InstalledTargetKernelInfo,
KernelCmdlineArg,
LateTargetKernelCmdlineArgTasks,
TargetKernelCmdlineArgTasks
)
from leapp.reporting import Report
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag

Expand All @@ -14,7 +20,13 @@ class KernelCmdlineConfig(Actor):
"""

name = 'kernelcmdlineconfig'
consumes = (KernelCmdlineArg, InstalledTargetKernelInfo, FirmwareFacts, TargetKernelCmdlineArgTasks)
consumes = (
KernelCmdlineArg,
InstalledTargetKernelInfo,
FirmwareFacts,
LateTargetKernelCmdlineArgTasks,
TargetKernelCmdlineArgTasks
)
produces = (Report,)
tags = (FinalizationPhaseTag, IPUWorkflowTag)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import itertools
import re

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries import stdlib
from leapp.libraries.common.config import architecture, version
from leapp.libraries.stdlib import api
from leapp.models import InstalledTargetKernelInfo, KernelCmdlineArg, TargetKernelCmdlineArgTasks
from leapp.models import (
InstalledTargetKernelInfo,
KernelCmdlineArg,
LateTargetKernelCmdlineArgTasks,
TargetKernelCmdlineArgTasks
)

KERNEL_CMDLINE_FILE = "/etc/kernel/cmdline"

Expand Down Expand Up @@ -71,7 +77,9 @@ def retrieve_arguments_to_modify():
kernelargs_msgs_to_add = list(api.consume(KernelCmdlineArg))
kernelargs_msgs_to_remove = []

for target_kernel_arg_task in api.consume(TargetKernelCmdlineArgTasks):
modification_msgs = itertools.chain(api.consume(TargetKernelCmdlineArgTasks),
api.consume(LateTargetKernelCmdlineArgTasks))
for target_kernel_arg_task in modification_msgs:
kernelargs_msgs_to_add.extend(target_kernel_arg_task.to_add)
kernelargs_msgs_to_remove.extend(target_kernel_arg_task.to_remove)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import re

from leapp.libraries.common.config import get_env
from leapp.libraries.common.config import get_env, version
from leapp.libraries.stdlib import api
from leapp.models import (
InitrdIncludes,
Expand Down Expand Up @@ -39,6 +39,9 @@ def generate_link_file(interface):

@suppress_deprecation(InitrdIncludes)
def process():
if get_env('LEAPP_USE_NET_NAMING_SCHEMES', '0') == '1' and version.get_target_major_version() != '8':
api.current_logger().info('Skipping generation of .link files renaming NICs as LEAPP_USE_NET_NAMING_SCHEMES=1')
return

if get_env('LEAPP_NO_NETWORK_RENAMING', '0') == '1':
api.current_logger().info(
Expand Down
21 changes: 21 additions & 0 deletions repos/system_upgrade/common/models/kernelcmdlineargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ class TargetKernelCmdlineArgTasks(Model):
to_remove = fields.List(fields.Model(KernelCmdlineArg), default=[])


class LateTargetKernelCmdlineArgTasks(Model):
"""
Desired modifications of the target kernel args produced later in the upgrade process.
Defined to prevent loops in the actor dependency graph.
"""
topic = SystemInfoTopic

to_add = fields.List(fields.Model(KernelCmdlineArg), default=[])
to_remove = fields.List(fields.Model(KernelCmdlineArg), default=[])


class UpgradeKernelCmdlineArgTasks(Model):
"""
Modifications of the upgrade kernel cmdline.
"""
topic = SystemInfoTopic

to_add = fields.List(fields.Model(KernelCmdlineArg), default=[])


class KernelCmdline(Model):
"""
Kernel command line parameters the system was booted with
Expand Down
Loading

0 comments on commit 6a32159

Please sign in to comment.