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

Include mdadm configuration in the upgrade initramfs #1095

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ build() {
if [[ -n "$LEAPP_DRACUT_LVMCONF" ]]; then
DRACUT_LVMCONF_ARG="--lvmconf"
fi
DRACUT_MDADMCONF_ARG="--nomdadmconf"
if [[ -n "$LEAPP_DRACUT_MDADMCONF" ]]; then
# include local /etc/mdadm.conf
DRACUT_MDADMCONF_ARG="--mdadmconf"

# include local /etc/mdadm.conf
DRACUT_MDADMCONF_ARG="--mdadmconf"
if [[ -n "$LEAPP_DRACUT_NO_MDADMCONF" ]]; then
DRACUT_MDADMCONF_ARG="--nomdadmconf"
fi

KERNEL_VERSION=$LEAPP_KERNEL_VERSION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,75 @@
from leapp.libraries.stdlib import api
from leapp.models import CopyFile, TargetUserSpacePreupgradeTasks

# Maps src location in the source system to the destination within the target system
FILES_TO_COPY_IF_PRESENT = {
'/etc/hosts': '/etc/hosts'
}

class FileToCopy:
"""
A file to be copied into target userspace

Also see DirToCopy
"""
def __init__(self, src_path, dst_path=None, fallback=None):
"""
Initialize a new FileToCopy

The file at src_path on the source system is to be copied to the
dst_path in the target userspace. The fallback argument allows creating
a chain of fallback files to try if the original file doesn't
exist(practically a linked list).

:param src_path: The path to the file on the source system
:param dst_path: The path in the target userspace, or src_path if not given
:param fallback: A file to try next if src_path doesn't exist
"""
self.src_path = src_path
self.dst_path = dst_path if dst_path else src_path
self.fallback = fallback

def check_filetype(self):
return os.path.isfile(self.src_path)


class DirToCopy(FileToCopy):
"""
A directory to be copied into target userspace

Also see FileToCopy
"""
def check_filetype(self):
return os.path.isdir(self.src_path)


# list of files and directories to copy into target userspace
FILES_TO_COPY_IF_PRESENT = [
FileToCopy('/etc/hosts'),
# the fallback usually goes to /etc/mdadm/mdadm.conf, however the /etc/mdadm dir
# doesn't exist in the target userspace and the targetuserspacecreator expects
# the destination dir to exist, so let's copy to /etc/. dracut also copies it there
FileToCopy('/etc/mdadm.conf', fallback=FileToCopy('/etc/mdadm/mdadm.conf', '/etc/mdadm.conf')),
# copy to /etc/mdadm.conf.d/, dracut only copies this one into initramfs and doesn't check the alternate one
DirToCopy('/etc/mdadm.conf.d/', fallback=FileToCopy('/etc/mdadm/mdadm.conf.d/', '/etc/mdadm.conf.d/'))
]


def _scan_file_to_copy(file):
"""
Scan the source system and identify file that should be copied into target userspace.

If the file doesn't exists or isn't of the right type it's fallbacks are searched, if set.

:return: The found file or None
:rtype: CopyFile | None
"""
tmp = file
while tmp and not tmp.check_filetype():
tmp = tmp.fallback

if not tmp:
api.current_logger().warning(
"File {} and its fallbacks do not exist or are not a correct filetype".format(file.src_path))
return None

return CopyFile(src=tmp.src_path, dst=tmp.dst_path)


def scan_files_to_copy():
Expand All @@ -16,10 +81,10 @@ def scan_files_to_copy():
When an item to be copied is identified a message :class:`CopyFile` is produced.
"""
files_to_copy = []
for src_path in FILES_TO_COPY_IF_PRESENT:
if os.path.isfile(src_path):
dst_path = FILES_TO_COPY_IF_PRESENT[src_path]
files_to_copy.append(CopyFile(src=src_path, dst=dst_path))
for file in FILES_TO_COPY_IF_PRESENT:
file_to_copy = _scan_file_to_copy(file)
if file_to_copy:
files_to_copy.append(file_to_copy)

preupgrade_task = TargetUserSpacePreupgradeTasks(copy_files=files_to_copy)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,88 +2,117 @@

import pytest

from leapp.libraries.actor.scanfilesfortargetuserspace import scan_files_to_copy
from leapp.libraries.common.testutils import produce_mocked
from leapp.libraries.actor import scanfilesfortargetuserspace
from leapp.libraries.actor.scanfilesfortargetuserspace import DirToCopy, FileToCopy
from leapp.libraries.common.testutils import produce_mocked, logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import CopyFile


@pytest.fixture
def isfile_default_config():
config = {
'/etc/hosts': True
}
return config
def _do_files_to_copy_contain_entry(copy_files, file_to_copy):
"""Searches the files to be copied for an entry with src field that matches the given src."""
for copy_file in copy_files:
if (
copy_file.src == file_to_copy.src_path
and copy_file.dst == file_to_copy.dst_path
):
return True
return False


@pytest.mark.parametrize(
"files",
(
[],
[FileToCopy("/etc/file")],
[
FileToCopy("/etc/fileA"),
FileToCopy("/etc/abcd/file", "/etc/1234/file"),
DirToCopy("/etc/dirA/dirB/dirC/", fallback=DirToCopy("/fallback/dir")),
DirToCopy("/root_level_dir", "/different/target/dir"),
],
),
)
def test_copyfiles_produced(monkeypatch, files):
"""
Test that CopyFile models are correctly produced for all files to copy
"""
scanfilesfortargetuserspace.FILES_TO_COPY_IF_PRESENT = files

def _scan_file_to_copy_mocked(file):
return CopyFile(src=file.src_path, dst=file.dst_path)

def do_files_to_copy_contain_entry_with_src(files_to_copy, src):
"""Searches the files to be copied for an entry with src field that matches the given src."""
is_present = False
for file_to_copy in files_to_copy:
if file_to_copy.src == src:
is_present = True
break
return is_present
actor_produces = produce_mocked()

monkeypatch.setattr(
scanfilesfortargetuserspace, "_scan_file_to_copy", _scan_file_to_copy_mocked
)
monkeypatch.setattr(api, "produce", actor_produces)

def make_mocked_isfile(configuration):
"""
Creates mocked isfile function that returns values according the given configuration.
scanfilesfortargetuserspace.scan_files_to_copy()

The created function raises :class:`ValueError` should the unit under test try to "isfile"
a path that is not present in the configuration.
fail_msg = "Produced unexpected number of messages."
assert len(actor_produces.model_instances) == 1, fail_msg

One global mocked function with configuration is error prone as individual test would
have to return the configuration into the original state after execution.
"""
preupgrade_task_msg = actor_produces.model_instances[0]
assert len(preupgrade_task_msg.copy_files) == len(files)

def mocked_isfile(path):
if path in configuration:
return configuration[path]
error_msg = 'The actor tried to isfile a path that it should not. (path `{0}`)'
raise ValueError(error_msg.format(path))
return mocked_isfile
for file in files:
assert _do_files_to_copy_contain_entry(preupgrade_task_msg.copy_files, file)

fail_msg = "Produced message contains rpms to be installed,"
"however only copy files field should be populated."
assert not preupgrade_task_msg.install_rpms, fail_msg

def test_etc_hosts_present(monkeypatch, isfile_default_config):
"""Tests whether /etc/hosts is identified as "to be copied" into target userspace when it is present."""
mocked_isfile = make_mocked_isfile(isfile_default_config)
actor_produces = produce_mocked()
monkeypatch.setattr(os.path, 'isfile', mocked_isfile)
monkeypatch.setattr(api, 'produce', actor_produces)

scan_files_to_copy()
TEST_FILE_PATH = "/etc/file"
TEST_DIR_PATH = "/etc/dir"

fail_msg = 'Produced unexpected number of messages.'
assert len(actor_produces.model_instances) == 1, fail_msg

preupgrade_task_msg = actor_produces.model_instances[0]
@pytest.mark.parametrize("file_to_copy", (FileToCopy(TEST_FILE_PATH), DirToCopy(TEST_DIR_PATH)))
def test_copy_present(monkeypatch, file_to_copy):
"""Test that file to copy is found if present"""
monkeypatch.setattr(os.path, "isdir", lambda f: f == TEST_DIR_PATH)
monkeypatch.setattr(os.path, "isfile", lambda f: f == TEST_FILE_PATH)

fail_msg = 'Didn\'t identify any files to copy into target userspace (at least /etc/hosts should be).'
assert preupgrade_task_msg.copy_files, fail_msg
copy_file = scanfilesfortargetuserspace._scan_file_to_copy(file_to_copy)

should_copy_hostsfile = do_files_to_copy_contain_entry_with_src(preupgrade_task_msg.copy_files, '/etc/hosts')
assert should_copy_hostsfile, 'Failed to identify /etc/hosts as a file to be copied into target userspace.'
assert copy_file
assert _do_files_to_copy_contain_entry([copy_file], file_to_copy)

fail_msg = 'Produced message contains rpms to be installed, however only copy files field should be populated.'
assert not preupgrade_task_msg.install_rpms, fail_msg

@pytest.mark.parametrize(
"file_to_copy",
[
FileToCopy("/etc/hosts"),
DirToCopy("/etc/dnf"),
FileToCopy("/etc/fileA", fallback=FileToCopy("/etc/fileB")),
],
)
def test_copy_missing(monkeypatch, file_to_copy):
"""Test that no file is found and returned if it isn't present"""
monkeypatch.setattr(os.path, "isfile", lambda _: False)
monkeypatch.setattr(os.path, "isdir", lambda _: False)
monkeypatch.setattr(api, 'current_logger', logger_mocked())

def test_etc_hosts_missing(monkeypatch, isfile_default_config):
"""Tests whether /etc/hosts is not identified as "to be copied" into target userspace when it is missing."""
isfile_default_config['/etc/hosts'] = False # The file is not present or is a directory (-> should not be copied)
mocked_isfile = make_mocked_isfile(isfile_default_config)
actor_produces = produce_mocked()
copy_file = scanfilesfortargetuserspace._scan_file_to_copy(file_to_copy)
assert not copy_file, "Should return None for a missing file"

monkeypatch.setattr(os.path, 'isfile', mocked_isfile)
monkeypatch.setattr(api, 'produce', actor_produces)
assert len(api.current_logger.warnmsg) == 1
assert file_to_copy.src_path in api.current_logger.warnmsg[0]

scan_files_to_copy()

assert len(actor_produces.model_instances) == 1, 'Produced unexpected number of messages.'
def test_copy_missing_with_fallback(monkeypatch):
"""Test that fallback is found and returned if the original file is not present"""
ORIG = "/etc/mdadm.conf"
FALLBACK = "/etc/mdadm/mdadm.conf"

preupgrade_task_msg = actor_produces.model_instances[0]
should_copy_hostsfile = do_files_to_copy_contain_entry_with_src(preupgrade_task_msg.copy_files, '/etc/hosts')
assert not should_copy_hostsfile, 'Identified /etc/hosts as a file to be copied even if it doesn\'t exists'
monkeypatch.setattr(os.path, "isfile", lambda f: f == FALLBACK)

fail_msg = 'Produced message contains rpms to be installed, however only copy files field should be populated.'
assert not preupgrade_task_msg.install_rpms, fail_msg
fallback_file = FileToCopy(FALLBACK)
file_to_scan = FileToCopy(ORIG, fallback=fallback_file)
copy_file = scanfilesfortargetuserspace._scan_file_to_copy(file_to_scan)

assert copy_file
assert copy_file.src == FALLBACK
assert copy_file.dst == FALLBACK