Skip to content

Commit

Permalink
live_mode: add new actors implementing the live mode functionality
Browse files Browse the repository at this point in the history
Add actors that scan the new configuration file devel-livemode.ini,
informing the rest of the actor collective about the configuration.
Based on this configuration, additional packages are requested to be
installed into the target userspace. The target userspace is also
modified to contain services that execute leapp based on kernel cmdline.
For a full list of modifications, see models/livemode.py added in
a previous commit.

The feature can be enabled by setting LEAPP_UNSUPPORTED=1 together
with LEAPP_DEVEL_ENABLE_LIVE_MODE=1. Note, that the squashfs-tools
package must be installed (otherwise an error will be raised). The
live mode feature is currently tested only for x86_64, and, therefore,
attempting to use this feature on a different architecture will be
prohibited by the implementation.

Jira ref: RHEL-45280
  • Loading branch information
Michal Hecko committed Aug 15, 2024
1 parent 89874fd commit 9413566
Show file tree
Hide file tree
Showing 23 changed files with 2,186 additions and 0 deletions.
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from leapp.actors import Actor
from leapp.libraries.actor import emit_livemode_userspace_requirements as emit_livemode_userspace_requirements_lib
from leapp.models import LiveModeConfig, TargetUserSpaceUpgradeTasks
from leapp.tags import ExperimentalTag, InterimPreparationPhaseTag, IPUWorkflowTag


class EmitLiveModeRequirements(Actor):
"""
Request addiontal packages to be installed into target userspace.
Additional packages can be requested using LiveModeConfig.additional_packages
"""

name = 'emit_livemode_requirements'
consumes = (LiveModeConfig,)
produces = (TargetUserSpaceUpgradeTasks,)
tags = (ExperimentalTag, InterimPreparationPhaseTag, IPUWorkflowTag,)

def process(self):
emit_livemode_userspace_requirements_lib.emit_livemode_userspace_requirements()
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from leapp.libraries.stdlib import api
from leapp.models import LiveModeConfig, TargetUserSpaceUpgradeTasks

# NOTE: would also need
# _REQUIRED_PACKAGES from actors/commonleappdracutmodules/libraries/modscan.py

_REQUIRED_PACKAGES_FOR_LIVE_MODE = [
'systemd-container',
'dbus-daemon',
'NetworkManager',
'util-linux',
'dracut-live',
'dracut-squash',
'dmidecode',
'pciutils',
'lsscsi',
'passwd',
'kexec-tools',
'vi',
'less',
'openssh-clients',
'strace',
'tcpdump',
]


def emit_livemode_userspace_requirements():
livemode_config = next(api.consume(LiveModeConfig), None)
if not livemode_config or not livemode_config.is_enabled:
return

packages = _REQUIRED_PACKAGES_FOR_LIVE_MODE + livemode_config.additional_packages
if livemode_config.setup_opensshd_with_auth_keys:
packages += ['openssh-server', 'crypto-policies']

packages = sorted(set(packages))

api.produce(TargetUserSpaceUpgradeTasks(install_rpms=packages))
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest

from leapp.libraries.actor import emit_livemode_userspace_requirements as emit_livemode_userspace_requirements_lib
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import LiveModeConfig, TargetUserSpaceUpgradeTasks


@pytest.mark.parametrize('livemode_config', (None, LiveModeConfig(squashfs_fullpath='<squashfs>', is_enabled=False)))
def test_no_emit_if_livemode_disabled(monkeypatch, livemode_config):
messages = [livemode_config] if livemode_config else []
actor_mock = CurrentActorMocked(msgs=messages)
monkeypatch.setattr(api, 'current_actor', actor_mock)
monkeypatch.setattr(api, 'produce', produce_mocked())

emit_livemode_userspace_requirements_lib.emit_livemode_userspace_requirements()

assert not api.produce.called


def test_emit(monkeypatch):
config = LiveModeConfig(squashfs_fullpath='<squashfs_path>', is_enabled=True, additional_packages=['EXTRA_PKG'])
actor_mock = CurrentActorMocked(msgs=[config])
monkeypatch.setattr(api, 'current_actor', actor_mock)
monkeypatch.setattr(api, 'produce', produce_mocked())

emit_livemode_userspace_requirements_lib.emit_livemode_userspace_requirements()

assert api.produce.called
assert len(api.produce.model_instances) == 1

required_pkgs = api.produce.model_instances[0]
assert isinstance(required_pkgs, TargetUserSpaceUpgradeTasks)

assert 'dracut-live' in required_pkgs.install_rpms
assert 'dracut-squash' in required_pkgs.install_rpms
assert 'EXTRA_PKG' in required_pkgs.install_rpms
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from leapp.actors import Actor
from leapp.libraries.actor.liveimagegenerator import generate_live_image_if_enabled
from leapp.models import (
LiveImagePreparationInfo,
LiveModeArtifacts,
LiveModeConfig,
LiveModeRequirementsTasks,
PrepareLiveImagePostTasks,
TargetUserSpaceInfo
)
from leapp.tags import ExperimentalTag, InterimPreparationPhaseTag, IPUWorkflowTag


class LiveImageGenerator(Actor):
"""
Generates the squashfs image for the livemode upgrade
"""

name = 'live_image_generator'
consumes = (LiveModeConfig,
LiveModeRequirementsTasks,
LiveImagePreparationInfo,
PrepareLiveImagePostTasks,
TargetUserSpaceInfo,)
produces = (LiveModeArtifacts,)
tags = (ExperimentalTag, InterimPreparationPhaseTag, IPUWorkflowTag,)

def process(self):
generate_live_image_if_enabled()
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import os
import os.path
import shutil

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import mounting
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import LiveModeArtifacts, LiveModeConfig, TargetUserSpaceInfo


def lighten_target_userpace(context):
"""
Remove unneeded files from the target userspace.
"""

userspace_trees_to_prune = ['artifacts', 'boot']

for tree_to_prune in userspace_trees_to_prune:
tree_full_path = os.path.join(context.base_dir, tree_to_prune)
try:
shutil.rmtree(tree_full_path)
except OSError as error:
api.current_logger().warning('Failed to remove /%s directory from the live image. Full error: %s',
tree_to_prune, error)


def build_squashfs(livemode_config, userspace_info):
"""
Generate the live rootfs image based on the target userspace
:param livemode LiveModeConfig: Livemode configuration message
:param userspace_info TargetUserspaceInfo: Information about how target userspace is set up
"""
target_userspace_fullpath = userspace_info.path
squashfs_fullpath = livemode_config.squashfs_fullpath

api.current_logger().info('Building the squashfs image %s from target userspace located at %s',
squashfs_fullpath, target_userspace_fullpath)

try:
if os.path.exists(squashfs_fullpath):
os.unlink(squashfs_fullpath)
except OSError as error:
api.current_logger().warning('Failed to remove already existing %s. Full error: %s',
squashfs_fullpath, error)

try:
run(['mksquashfs', target_userspace_fullpath, squashfs_fullpath])
except CalledProcessError as error:
raise StopActorExecutionError(
'Cannot pack the target userspace into a squash image. ',
details={'details': 'The following error occurred while building the squashfs image: {0}.'.format(error)}
)

return squashfs_fullpath


def generate_live_image_if_enabled():
"""
Main function to generate the additional artifacts needed to run in live mode.
"""

livemode_config = next(api.consume(LiveModeConfig), None)
if not livemode_config or not livemode_config.is_enabled:
return

userspace_info = next(api.consume(TargetUserSpaceInfo), None)

with mounting.NspawnActions(base_dir=userspace_info.path) as context:
lighten_target_userpace(context)
squashfs_path = build_squashfs(livemode_config, userspace_info)
api.produce(LiveModeArtifacts(squashfs_path=squashfs_path))
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import collections
import os
import shutil

import pytest

from leapp.libraries.actor import liveimagegenerator as live_image_generator_lib
from leapp.libraries.common import mounting
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import LiveModeArtifacts, LiveModeConfig, TargetUserSpaceInfo


def test_squafs_creation(monkeypatch):
userspace_info = TargetUserSpaceInfo(path='/USERSPACE', scratch='/SCRATCH', mounts='/MOUNTS')
livemode_config = LiveModeConfig(is_enabled=True, squashfs_fullpath='/var/lib/leapp/squashfs.img')

def exists_mock(path):
assert path == '/var/lib/leapp/squashfs.img'
return True

monkeypatch.setattr(os.path, 'exists', exists_mock)

def unlink_mock(path):
assert path == '/var/lib/leapp/squashfs.img'

monkeypatch.setattr(os, 'unlink', unlink_mock)

commands_executed = []

def run_mock(command):
commands_executed.append(command[0])

monkeypatch.setattr(live_image_generator_lib, 'run', run_mock)

live_image_generator_lib.build_squashfs(livemode_config, userspace_info)
assert commands_executed == ['mksquashfs']


def test_userspace_lightening(monkeypatch):

removed_trees = []

def rmtree_mock(path):
removed_trees.append(path)

monkeypatch.setattr(shutil, 'rmtree', rmtree_mock)

_ContextMock = collections.namedtuple('ContextMock', ('base_dir'))
context_mock = _ContextMock(base_dir='/USERSPACE')

live_image_generator_lib.lighten_target_userpace(context_mock)

assert removed_trees == ['/USERSPACE/artifacts', '/USERSPACE/boot']


@pytest.mark.parametrize(
('livemode_config', 'should_produce'),
(
(LiveModeConfig(is_enabled=True, squashfs_fullpath='/squashfs'), True),
(LiveModeConfig(is_enabled=False, squashfs_fullpath='/squashfs'), False),
(None, False),
)
)
def test_generate_live_image_if_enabled(monkeypatch, livemode_config, should_produce):
userspace_info = TargetUserSpaceInfo(path='/USERSPACE', scratch='/SCRATCH', mounts='/MOUNTS')
messages = [livemode_config, userspace_info] if livemode_config else [userspace_info]
actor_mock = CurrentActorMocked(msgs=messages)
monkeypatch.setattr(api, 'current_actor', actor_mock)

class NspawnMock:
def __init__(self, *args, **kwargs):
pass

def __enter__(self, *args, **kwargs):
pass

def __exit__(self, *args, **kwargs):
pass

monkeypatch.setattr(mounting, 'NspawnActions', NspawnMock)
monkeypatch.setattr(live_image_generator_lib, 'lighten_target_userpace', lambda context: None)
monkeypatch.setattr(live_image_generator_lib, 'build_squashfs',
lambda livemode_config, userspace_info: '/squashfs')
monkeypatch.setattr(api, 'produce', produce_mocked())

live_image_generator_lib.generate_live_image_if_enabled()

if should_produce:
assert api.produce.called
assert len(api.produce.model_instances) == 1
artifacts = api.produce.model_instances[0]
assert isinstance(artifacts, LiveModeArtifacts)
assert artifacts.squashfs_path == '/squashfs'
else:
assert not api.produce.called
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from leapp.actors import Actor
from leapp.libraries.actor import scan_livemode_config as scan_livemode_config_lib
from leapp.models import InstalledRPM, LiveModeConfig
from leapp.tags import ExperimentalTag, FactsPhaseTag, IPUWorkflowTag


class LiveModeConfigScanner(Actor):
"""
Read livemode configuration located at /etc/leapp/files/devel-livemode.ini
"""

name = 'live_mode_config_scanner'
consumes = (InstalledRPM,)
produces = (LiveModeConfig,)
tags = (ExperimentalTag, FactsPhaseTag, IPUWorkflowTag,)

def process(self):
scan_livemode_config_lib.scan_config_and_emit_message()
Loading

0 comments on commit 9413566

Please sign in to comment.