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

Introduce experimental live mode [squashfs] #1248

Merged
merged 3 commits into from
Aug 16, 2024
Merged
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
9 changes: 9 additions & 0 deletions etc/leapp/files/devel-livemode.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Configuration for the *experimental* livemode feature
# It is likely that this entire configuration file will be replaced by some
# other mechanism/file in the future. For the full list of configuration options,
# see models/livemode.py
[livemode]
squashfs_fullpath=/var/lib/leapp/live-upgrade.img
setup_network_manager=no
autostart_upgrade_after_reboot=yes
setup_passwordless_root=no
23 changes: 21 additions & 2 deletions repos/system_upgrade/common/actors/addupgradebootentry/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor.addupgradebootentry import add_boot_entry, fix_grub_config_error
from leapp.models import BootContent, FirmwareFacts, GrubConfigError, TargetKernelCmdlineArgTasks, TransactionDryRun
from leapp.models import (
BootContent,
FirmwareFacts,
GrubConfigError,
KernelCmdline,
LiveImagePreparationInfo,
LiveModeArtifacts,
LiveModeConfig,
TargetKernelCmdlineArgTasks,
TransactionDryRun
)
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag


Expand All @@ -15,7 +25,16 @@ class AddUpgradeBootEntry(Actor):
"""

name = 'add_upgrade_boot_entry'
consumes = (BootContent, GrubConfigError, FirmwareFacts, TransactionDryRun)
consumes = (
BootContent,
GrubConfigError,
FirmwareFacts,
LiveImagePreparationInfo,
LiveModeArtifacts,
LiveModeConfig,
KernelCmdline,
TransactionDryRun
)
produces = (TargetKernelCmdlineArgTasks,)
tags = (IPUWorkflowTag, InterimPreparationPhaseTag)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,152 @@
import itertools
import os
import re

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import architecture
from leapp.libraries.common.config import architecture, get_env
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import BootContent, KernelCmdlineArg, TargetKernelCmdlineArgTasks
from leapp.models import (
BootContent,
KernelCmdline,
KernelCmdlineArg,
LiveImagePreparationInfo,
LiveModeArtifacts,
LiveModeConfig,
TargetKernelCmdlineArgTasks
)


def collect_boot_args(livemode_enabled):
args = {
'enforcing': '0',
'rd.plymouth': '0',
'plymouth.enable': '0'
}

if get_env('LEAPP_DEBUG', '0') == '1':
args['debug'] = None

if get_env('LEAPP_DEVEL_INITRAM_NETWORK') in ('network-manager', 'scripts'):
args['ip'] = 'dhcp'
args['rd.neednet'] = '1'

if livemode_enabled:
livemode_args = construct_cmdline_args_for_livemode()
args.update(livemode_args)

return args


def collect_undesired_args(livemode_enabled):
args = {}
if livemode_enabled:
args = dict(zip(('ro', 'rhgb', 'quiet'), itertools.repeat(None)))
args['rd.lvm.lv'] = _get_rdlvm_arg_values()

return args


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

def fmt_single_arg(arg_pair):
key, value = arg_pair
if not value:
return str(key)
return '{key}={value}'.format(key=key, value=value)

def flatten_arguments(arg_pair):
""" Expand multi-valued values into an iterable (key, value1), (key, value2) """
key, value = arg_pair
if isinstance(value, (tuple, list)):
# value is multi-valued (a tuple of values)
for value_elem in value: # yield from is not available in python2.7
yield (key, value_elem)
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()))

# 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.
cmdline_pieces = (fmt_single_arg(arg_pair) for arg_pair in sorted(arg_sequence))
cmdline = ' '.join(cmdline_pieces)

return cmdline


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)

create_entry_cmd = [
'/usr/sbin/grubby',
'--add-kernel', '{0}'.format(kernel_path),
'--initrd', '{0}'.format(initramfs_path),
'--title', 'RHEL-Upgrade-Initramfs',
'--copy-default',
'--make-default',
'--args', args_to_add_str
]
boot_entry_modification_commands.append(create_entry_cmd)

# 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)
if root_param_value:
enforce_root_param_for_the_entry_cmd = [
'/usr/sbin/grubby',
'--update-kernel', kernel_path,
'--args', 'root={0}'.format(root_param_value)
]
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)
remove_undesired_args_cmd = [
'/usr/sbin/grubby',
'--update-kernel', kernel_path,
'--remove-args', args_to_remove_str
]
boot_entry_modification_commands.append(remove_undesired_args_cmd)
return boot_entry_modification_commands


def add_boot_entry(configs=None):
debug = 'debug' if os.getenv('LEAPP_DEBUG', '0') == '1' else ''
enable_network = os.getenv('LEAPP_DEVEL_INITRAM_NETWORK') in ('network-manager', 'scripts')
ip_arg = ' ip=dhcp rd.neednet=1' if enable_network else ''
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)
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_remove=undesired_cmdline_args)

def run_commands_adding_entry(extra_command_suffix=None):
if not extra_command_suffix:
extra_command_suffix = []
for command in commands_to_run:
run(command + extra_command_suffix)

try:
cmd = [
'/usr/sbin/grubby',
'--add-kernel', '{0}'.format(kernel_dst_path),
'--initrd', '{0}'.format(initram_dst_path),
'--title', 'RHEL-Upgrade-Initramfs',
'--copy-default',
'--make-default',
'--args', '{DEBUG}{NET} enforcing=0 rd.plymouth=0 plymouth.enable=0'.format(DEBUG=debug, NET=ip_arg)
]
if configs:
for config in configs:
run(cmd + ['-c', config])
run_commands_adding_entry(extra_command_suffix=['-c', config])
else:
run(cmd)
run_commands_adding_entry(extra_command_suffix=None)

if architecture.matches_architecture(architecture.ARCH_S390X):
# on s390x we need to call zipl explicitly because of issue in grubby,
# otherwise the new boot entry will not be set as default
# See https://bugzilla.redhat.com/show_bug.cgi?id=1764306
run(['/usr/sbin/zipl'])

if debug:
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.
Expand Down Expand Up @@ -114,3 +225,101 @@ def fix_grub_config_error(conf_file, error_type):

elif error_type == 'missing newline':
write_to_file(conf_file, config + '\n')


def local_os_stat(path):
""" Local wrapper around os.stat so we can safely mock it in tests. """
return os.stat(path)
MichalHe marked this conversation as resolved.
Show resolved Hide resolved


def _get_device_uuid(path):
"""
Find the UUID of a device in which the given path is located.
"""
while not os.path.ismount(path):
path = os.path.dirname(path)

needle_dev_id = local_os_stat(path).st_dev

for uuid in os.listdir('/dev/disk/by-uuid'):
uuid_fullpath = os.path.join('/dev/disk/by-uuid/', uuid)
dev_path = os.readlink(uuid_fullpath)

# The link target is likely relative to the UUID_fullpath, e.g., ../../dm-1.
# Joining it will '/dev/disk/by-uuid' will resolve the relative path.
# If dev_path is absolute it returns dev_path.
dev_path = os.path.join('/dev/disk/by-uuid', dev_path)
dev_path = os.path.abspath(dev_path)

dev_id = local_os_stat(dev_path).st_rdev
if dev_id == needle_dev_id:
return uuid

return None


def _get_rdlvm_arg_values():
# should we not check args returned by grubby instead?
MichalHe marked this conversation as resolved.
Show resolved Hide resolved
cmdline_msg = next(api.consume(KernelCmdline), None)

if not cmdline_msg:
raise StopActorExecutionError('Did not receive any KernelCmdline arguments.')

rd_lvm_values = sorted(arg.value for arg in cmdline_msg.parameters if arg.key == 'rd.lvm.lv')
api.current_logger().debug('Collected the following rd.lvm.lv args that are undesired for the squashfs: %s',
rd_lvm_values)

return rd_lvm_values


def construct_cmdline_args_for_livemode():
"""
Prepare cmdline parameters for the live mode
"""
# boot locally by default

livemode_config = next(api.consume(LiveModeConfig), None)
if not livemode_config:
raise StopActorExecutionError('Did not receive any livemode configuration message although it is enabled.')

livemode_artifacts = next(api.consume(LiveModeArtifacts), None)
if not livemode_artifacts:
raise StopActorExecutionError('Did not receive any livemode artifacts message although it is enabled.')

liveimg_filename = os.path.basename(livemode_artifacts.squashfs_path)
dir_path_containing_liveimg = os.path.dirname(livemode_artifacts.squashfs_path)

args = {'rw': None}

# if an URL is defined, boot over the network (http, nfs, ftp, ...)
if livemode_config.url_to_load_squashfs_from:
args['root'] = 'live:{}'.format(livemode_config.url_to_load_squashfs_from)
else:
args['root'] = 'live:UUID={}'.format(_get_device_uuid(dir_path_containing_liveimg))
args['rd.live.dir'] = dir_path_containing_liveimg
args['rd.live.squashimg'] = liveimg_filename

if livemode_config.dracut_network:
network_fragments = livemode_config.dracut_network.split('=', 1)

# @Todo(mhecko): verify this during config scan
if len(network_fragments) == 1 or network_fragments[0] != 'ip':
msg = ('The livemode dracut_network configuration value is incorrect - it does not '
'have the form of a key=value cmdline arg: `{0}`.')
msg = msg.format(livemode_config.dracut_network)

api.current_logger().error(msg)
raise StopActorExecutionError('Livemode is not configured correctly.', details={'details': msg})

args['ip'] = network_fragments[1]
args['rd.needsnet'] = '1'

autostart_state = '1' if livemode_config.autostart_upgrade_after_reboot else '0'
args['upgrade.autostart'] = autostart_state

if livemode_config.capture_upgrade_strace_into:
args['upgrade.strace'] = livemode_config.capture_upgrade_strace_into

api.current_logger().info('The use of live mode image implies the following cmdline args: %s', args)

return args
Loading