ault if no channel was specified.
    The channel returned is always lower case regardless how it was specified by the user.

    The target channel can be specified via:
        - Using the environment variable LEAPP_DEVEL_TARGET_PRODUCT_TYPE (devel variable with higher priority than
        any other way of specifying target channel).
        - Using the environment variable LEAPP_TARGET_PRODUCT_CHANNEL
        - Using the '--channel' option when runnning leapp preupgrade/upgrade

    :param default: Value to be returned if no target product type has been specified when running leapp.
    :type default: str
    :returns: The user-specified target channel or default if no channel was specified.
    :rtype: str
    """

    devel_target_product_type = get_env('LEAPP_DEVEL_TARGET_PRODUCT_TYPE')
    if devel_target_product_type:
        devel_target_product_type = devel_target_product_type.lower()
        supported_devel_target_channels = SUPPORTED_TARGET_CHANNELS | {'beta'}
        if devel_target_product_type not in supported_devel_target_channels:
            _stop_exection_due_invalid_channel('LEAPP_DEVEL_TARGET_PRODUCT_TYPE',
                                               devel_target_product_type,
                                               supported_devel_target_channels)
        return devel_target_product_type

    target_product_channel = get_env('LEAPP_TARGET_PRODUCT_CHANNEL')
    if target_product_channel:
        target_product_channel = target_product_channel.lower()
        if target_product_channel not in SUPPORTED_TARGET_CHANNELS:
            _stop_exection_due_invalid_channel('LEAPP_TARGET_PRODUCT_CHANNEL or the --channel option',
                                               target_product_channel,
                                               SUPPORTED_TARGET_CHANNELS)
        return target_product_channel

    return default


# repos/system_upgrade/common/libraries/config/architecture.py

import six

from leapp.libraries.stdlib import api


ARCH_X86_64 = 'x86_64'
ARCH_ARM64 = 'aarch64'
ARCH_PPC64LE = 'ppc64le'
ARCH_S390X = 's390x'
ARCH_ACCEPTED = (ARCH_X86_64, ARCH_ARM64, ARCH_PPC64LE, ARCH_S390X)
ARCH_SUPPORTED = (ARCH_X86_64, ARCH_ARM64, ARCH_PPC64LE, ARCH_S390X)


def matches_architecture(*match_list):
    """
    Check if one of provided architectures matches the system's one.

    :param match_list: specification of architectures to check against
    :type match_list: strings
    :return: ``True`` if system's architecture matches one of the values in match_list, ``False`` otherwise
    :rtype: bool
    """
    if not all(isinstance(e, six.string_types) for e in match_list):
        raise TypeError("Architectures to check against have to strings "
                        "but provided was {}: '{}'".format([type(e) for e in match_list], match_list))
    unsupported = set(match_list).difference(ARCH_ACCEPTED)
    if unsupported:
        api.current_logger().warning("Unsupported architecture specified: {}".format(unsupported))
    return api.current_actor().configuration.architecture in match_list


# repos/system_upgrade/common/libraries/config/mock_configs.py


"""
This is not regular library.

The library is supposed to be used only for testing purposes. Import of the
library is expected only inside test files.
"""

from leapp.models import IPUConfig, EnvVar, OSRelease, Version

CONFIG = IPUConfig(
    leapp_env_vars=[EnvVar(name='LEAPP_DEVEL', value='0')],
    os_release=OSRelease(
        release_id='rhel',
        name='Red Hat Enterprise Linux Server',
        pretty_name='RHEL',
        version='7.6 (Maipo)',
        version_id='7.6'
    ),
    version=Version(
        source='7.6',
        target='8.0'
    ),
    architecture='x86_64',
    kernel='3.10.0-957.43.1.el7.x86_64',
)

CONFIG_NO_NETWORK_RENAMING = IPUConfig(
    leapp_env_vars=[EnvVar(name='LEAPP_DEVEL', value='0'), EnvVar(name='LEAPP_NO_NETWORK_RENAMING', value='1')],
    os_release=OSRelease(
        release_id='rhel',
        name='Red Hat Enterprise Linux Server',
        pretty_name='RHEL',
        version='7.6 (Maipo)',
        version_id='7.6'
    ),
    version=Version(
        source='7.6',
        target='8.0'
    ),
    architecture='x86_64',
    kernel='3.10.0-957.43.1.el7.x86_64',
)

CONFIG_ALL_SIGNED = IPUConfig(
    leapp_env_vars=[EnvVar(name='LEAPP_DEVEL_RPMS_ALL_SIGNED', value='1')],
    os_release=OSRelease(
        release_id='rhel',
        name='Red Hat Enterprise Linux Server',
        pretty_name='RHEL',
        version='7.6 (Maipo)',
        version_id='7.6'
    ),
    version=Version(
        source='7.6',
        target='8.0'
    ),
    architecture='x86_64',
    kernel='3.10.0-957.43.1.el7.x86_64',
)

CONFIG_S390X = IPUConfig(
    os_release=OSRelease(
        release_id='rhel',
        name='Red Hat Enterprise Linux Server',
        pretty_name='RHEL',
        version='7.6 (Maipo)',
        version_id='7.6'
    ),
    version=Version(
        source='7.6',
        target='8.0'
    ),
    architecture='s390x',
    kernel='3.10.0-957.43.1.el7.x86_64',
)


# repos/system_upgrade/common/libraries/config/version.py

import operator

import six

from leapp.libraries.stdlib import api

OP_MAP = {
    '>': operator.gt,
    '>=': operator.ge,
    '<': operator.lt,
    '<=': operator.le
}

_SUPPORTED_VERSIONS = {
    # Note: 'rhel-alt' is detected when on 'rhel' with kernel 4.x
    '7': {'rhel': ['7.9'], 'rhel-alt': ['7.6'], 'rhel-saphana': ['7.9']},
    '8': {'rhel': ['8.5', '8.6']},
}


def get_major_version(version):
    """
    Return the major version from the given version string.

    Versioning schema: MAJOR.MINOR.PATCH
    It doesn't matter how many dots are present. Everything until the first dot is returned. E.g.:
       8.1.0 => 8
       7.9   => 7
       7     => 7

    :param str version: The version string according to the versioning schema described.
    :rtype: str
    :returns: The major version from the given version string.
    """
    return version.split('.')[0]


def get_source_version():
    """
    Return the version of the source system.

    :rtype: str
    :returns: The version of the source system.
    """
    return api.current_actor().configuration.version.source


def get_source_major_version():
    """
    Return the major version of the source (original) system.

    For more details about about the versioning schema see :func:`get_major_version`.

    :rtype: str
    :returns: The major version of the source system.
    """
    return get_major_version(get_source_version())


def get_target_version():
    """
    Return the version of the target system.

    :rtype: str
    :returns: The version of the target system.
    """
    return api.current_actor().configuration.version.target


def get_target_major_version():
    """
    Return the major version of the target system.

    For more details about about the versioning schema see :func:`get_major_version`.

    :rtype: str
    :returns: The major version of the target system.
    """
    return get_major_version(get_target_version())


class _SupportedVersionsDict(dict):
    """
    Class for _SUPPORTED_VERSIONS lazy evaluation until ipuworkflowconfig actor data
    is ready.
    """

    def __init__(self):  # pylint: disable=W0231
        self.data = {}

    def _feed_supported_versions(self):
        major = get_source_major_version()
        if major not in _SUPPORTED_VERSIONS:
            raise KeyError('{} is not a supported source version of RHEL'.format(major))
        self.data = _SUPPORTED_VERSIONS[major]

    def __getitem__(self, key):
        self._feed_supported_versions()
        return self.data[key]

    def __iter__(self):
        self._feed_supported_versions()
        for d in self.data:
            yield d

    def __repr__(self):
        self._feed_supported_versions()
        return repr(self.data)

    def __contains__(self, x):
        self._feed_supported_versions()
        return x in self.data

    def __len__(self):
        self._feed_supported_versions()
        return len(self.data)

    def __str__(self):
        self._feed_supported_versions()
        return str(self.data)


SUPPORTED_VERSIONS = _SupportedVersionsDict()


def _version_to_tuple(version):
    """Converts the version string ``major.minor`` to ``(major, minor)`` int tuple."""
    major, minor = version.split('.')
    return (int(major), int(minor))


def _validate_versions(versions):
    """Raise ``TypeError`` if provided versions are not strings in the form ``<integer>.<integer>``."""
    for ver in versions:
        split = ver.split('.')
        if not len(split) == 2 or not all(x.isdigit() for x in split):
            raise ValueError("Versions have to be in the form of '<integer>.<integer>' "
                             "but provided was '{}'".format(versions))


def _simple_versions(versions):
    """Return ``True`` if provided versions are list of strings without comparison operators."""
    return all(len(v.split()) == 1 for v in versions)


def _cmp_versions(versions):
    """Return ``True`` if provided versions are list of strings with comparison operators."""
    split = [v.split() for v in versions]
    if not all(len(s) == 2 for s in split):
        return False

    return all(s[0] in OP_MAP for s in split)


def matches_version(match_list, detected):
    """
    Check if the `detected` version meets the criteria specified in `match_list`.

    :param match_list: specification of versions to check against
    :type match_list: list or tuple of strings in one of the two following forms:
                      ``['>'|'>='|'<'|'<='] <integer>.<integer>`` form, where elements are ANDed,
                      meaning that ``['>= 7.6', '< 7.8']`` would match for ``'7.6'``, and ``'7,7'`` only.
                      ``<integer>.<integer>`` form, where elements are ORed, meaning that
                      ``['7.6', '7.7']`` would match for ``'7.6'``, and ``'7,7'`` only.
                      These two forms cannot be mixed, otherwise ``ValueError`` is raised.
    :param detected: detected version
    :type detected: string in the form ``<integer>.<integer>``
    :return: ``True`` if `detected` value matches one of the values in `match_list`, ``False`` otherwise
    :rtype: bool
    """
    if not isinstance(match_list, (list, tuple)):
        raise TypeError("Versions to check against have to be a list or tuple "
                        "but provided was {}: '{}'".format(type(match_list), match_list))
    if not all(isinstance(e, six.string_types) for e in match_list):
        raise TypeError("Versions to check against have to be a list or tuple of strings "
                        "but provided was {}: '{}'".format([type(e) for e in match_list], match_list))
    if not isinstance(detected, six.string_types):
        raise TypeError("Detected version has to be a string "
                        "but provided was {}: '{}'".format(type(detected), detected))
    _validate_versions([detected])

    if _simple_versions(match_list):
        # match_list = ['7.6', '7.7', '7.8', '7.9']
        _validate_versions(match_list)
        return detected in match_list
    if _cmp_versions(match_list):
        detected = _version_to_tuple(detected)
        # match_list = ['>= 7.6', '< 7.10']
        _validate_versions([s.split()[1] for s in match_list])
        for match in match_list:
            op, ver = match.split()
            ver = _version_to_tuple(ver)
            if not OP_MAP[op](detected, ver):
                return False
        return True

    raise ValueError("Versions have to be a list or tuple of strings in the form "
                     "'['>'|'>='|'<'|'<='] <integer>.<integer>' or "
                     "'<integer>.<integer>' but provided was '{}'".format(match_list))


def matches_source_version(*match_list):
    """
    Check if one of provided source versions matches the configured one.

    :param match_list: specification of versions to check against
    :type match_list: strings, for details see argument ``match_list`` of function :func:`matches_version`.
    """
    return matches_version(match_list, get_source_version())


def matches_target_version(*match_list):
    """
    Check if one of provided target versions matches the configured one.

    :param match_list: specification of versions to check against
    :type match_list: strings, for details see argument ``match_list`` of function :func:`matches_version`.
    """
    return matches_version(match_list, get_target_version())


def matches_release(allowed_releases, release):
    """
    Check if the given `release` is allowed to upgrade based in `allowed_releases`.

    :param allowed_releases: All supported releases
    :type allowed_releases: list or dict
    :param release: release name to be checked
    :type release: string
    :return: ``True`` if `release` value matches one of the values in `allowed_releases`, ``False`` otherwise
    :rtype: bool
    """
    if not (release and allowed_releases):
        return False

    return release in allowed_releases


def current_version():
    """
    Return the current Linux release and version.

    :return: The tuple contains release name and version value.
    :rtype: (string, string)
    """
    release = api.current_actor().configuration.os_release
    return release.release_id, release.version_id


def is_default_flavour():
    """
    Check if the current system uses the default upgrade path.

    :return: `True` if this upgrade process is using the default upgrade path and `False` otherwise.
    :rtype: bool
    """
    return api.current_actor().configuration.flavour == 'default'


def is_sap_hana_flavour():
    """
    Check if the current system needs to use the SAP HANA upgrade path.

    :return: `True` if this upgrade process is using the SAP HANA upgrade path and `False` otherwise.
    :rtype: bool
    """
    return api.current_actor().configuration.flavour == 'saphana'


def is_rhel_alt():
    """
    Check if the current system is RHEL-ALT or not (only for RHEL 7)

    The function is valid only for the RHEL 7 systems. On RHEL 8+ systems
    returns always False.

    :return: `True` if the current system is RHEL-ALT and `False` otherwise.
    :rtype: bool
    """

    if get_source_major_version() != '7':
        return False
    conf = api.current_actor().configuration
    # rhel-alt is rhel 7 with kernel 4.x - there is not better detection...
    return conf.os_release.release_id == 'rhel' and conf.kernel[0] == '4'


def is_rhel_realtime():
    """
    Check whether the original system is RHEL Real Time.

    Currently the check is based on the release of the original booted kernel.
    In case of RHEL, we are sure the release contains the ".rt" string and
    non-realtime kernels don't. Let's use this minimalistic check for now.
    In future, we could detect whether the system is preemptive or not based
    on properties of the kernel (e.g. uname -a tells that information).

    :return: `True` if the orig system is RHEL RT and `False` otherwise.
    :rtype: bool
    """
    conf = api.current_actor().configuration
    if conf.os_release.release_id != 'rhel':
        return False
    return '.rt' in conf.kernel.split('-')[1]


def is_supported_version():
    """
    Verify if the current system version is supported for the upgrade.

    :return: `True` if the current version is supported and `False` otherwise.
    :rtype: bool
    """
    release_id, version_id = current_version()
    if is_rhel_alt():
        release_id = 'rhel-alt'
    elif is_sap_hana_flavour():
        release_id = 'rhel-saphana'

    if not matches_release(SUPPORTED_VERSIONS, release_id):
        return False

    return matches_version(SUPPORTED_VERSIONS[release_id], version_id)


# repos/system_upgrade/common/libraries/tests/test_dnfplugin.py

from collections import namedtuple

import pytest

import leapp.models
from leapp.libraries.common import dnfplugin
from leapp.libraries.common.config.version import get_major_version
from leapp.models.fields import Boolean
from leapp.topics import Topic


class DATADnfPluginDataTopic(Topic):
    name = 'data_dnf_plugin_data'


fields = leapp.models.fields

TaskData = namedtuple('TaskData', 'expected initdata')

TEST_INSTALL_PACKAGES = TaskData(
    expected=('install1', 'install2'),
    initdata=('install1', 'install2')
)
TEST_REMOVE_PACKAGES = TaskData(
    expected=('remove1', 'remove2'),
    initdata=('remove1', 'remove2'),
)
TEST_UPGRADE_PACKAGES = TaskData(
    expected=('upgrade1', 'upgrade2'),
    initdata=('upgrade1', 'upgrade2'),
)
TEST_ENABLE_MODULES = TaskData(
    expected=('enable1:stream1', 'enable2:stream2'),
    initdata=(
        leapp.models.Module(name='enable1', stream='stream1'),
        leapp.models.Module(name='enable2', stream='stream2'),
    )
)


class DATADnfPluginDataPkgsInfo(leapp.models.Model):
    topic = DATADnfPluginDataTopic
    local_rpms = fields.List(fields.String())
    to_install = fields.List(fields.StringEnum(choices=TEST_INSTALL_PACKAGES.expected))
    to_remove = fields.List(fields.StringEnum(choices=TEST_REMOVE_PACKAGES.expected))
    to_upgrade = fields.List(fields.StringEnum(choices=TEST_UPGRADE_PACKAGES.expected))
    modules_to_enable = fields.List(fields.StringEnum(choices=TEST_ENABLE_MODULES.expected))


TEST_ENABLE_REPOS_CHOICES = ('enabled_repo', 'BASEOS', 'APPSTREAM')


class BooleanEnum(fields.EnumMixin, Boolean):
    pass


class DATADnfPluginDataDnfConf(leapp.models.Model):
    topic = DATADnfPluginDataTopic
    allow_erasing = BooleanEnum(choices=[True])
    best = BooleanEnum(choices=[True])
    debugsolver = fields.Boolean()
    disable_repos = BooleanEnum(choices=[True])
    enable_repos = fields.List(fields.StringEnum(choices=TEST_ENABLE_REPOS_CHOICES))
    gpgcheck = BooleanEnum(choices=[False])
    platform_id = fields.StringEnum(choices=['platform:el8', 'platform:el9'])
    releasever = fields.String()
    installroot = fields.StringEnum(choices=['/installroot'])
    test_flag = fields.Boolean()


class DATADnfPluginDataRHUIAWS(leapp.models.Model):
    topic = DATADnfPluginDataTopic
    on_aws = fields.Boolean()
    region = fields.Nullable(fields.String())


class DATADnfPluginDataRHUI(leapp.models.Model):
    topic = DATADnfPluginDataTopic
    aws = fields.Model(DATADnfPluginDataRHUIAWS)


class DATADnfPluginData(leapp.models.Model):
    topic = DATADnfPluginDataTopic
    pkgs_info = fields.Model(DATADnfPluginDataPkgsInfo)
    dnf_conf = fields.Model(DATADnfPluginDataDnfConf)
    rhui = fields.Model(DATADnfPluginDataRHUI)


# Delete those models from leapp.models to 'unpolute' the module
del leapp.models.DATADnfPluginDataPkgsInfo
del leapp.models.DATADnfPluginDataDnfConf
del leapp.models.DATADnfPluginDataRHUI
del leapp.models.DATADnfPluginDataRHUIAWS
del leapp.models.DATADnfPluginData


def _mocked_get_target_major_version(version):
    def impl():
        return version
    return impl


def _mocked_api_get_file_path(name):
    return 'some/random/file/path/{}'.format(name)


_CONFIG_BUILD_TEST_DEFINITION = (
    #   Parameter, Input Data, Expected Fields with data
    ('debug', False, ('dnf_conf', 'debugsolver'), False),
    ('debug', True, ('dnf_conf', 'debugsolver'), True),
    ('target_repoids', TEST_ENABLE_REPOS_CHOICES, ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES)),
    ('target_repoids', TEST_ENABLE_REPOS_CHOICES[0:1],
     ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES[0:1])),
    ('target_repoids', TEST_ENABLE_REPOS_CHOICES[1:],
     ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES[1:])),
    ('target_repoids', TEST_ENABLE_REPOS_CHOICES[2:],
     ('dnf_conf', 'enable_repos'), list(TEST_ENABLE_REPOS_CHOICES[2:])),
    ('test', False, ('dnf_conf', 'test_flag'), False),
    ('test', True, ('dnf_conf', 'test_flag'), True),
)


@pytest.mark.parametrize('used_target_version', ['8.4', '8.5', '9.0', '9.1'])
@pytest.mark.parametrize('parameter,input_value,test_path,expected_value', _CONFIG_BUILD_TEST_DEFINITION)
def test_build_plugin_data_variations(
    monkeypatch,
    used_target_version,
    parameter,
    input_value,
    test_path,
    expected_value,
):
    used_target_major_version = get_major_version(used_target_version)
    monkeypatch.setattr(dnfplugin, 'get_target_version', _mocked_get_target_major_version(used_target_version))
    monkeypatch.setattr(dnfplugin, 'get_target_major_version',
                        _mocked_get_target_major_version(used_target_major_version))
    inputs = {
        'target_repoids': ['BASEOS', 'APPSTREAM'],
        'debug': True,
        'test': True,
        'on_aws': False,
        'tasks': leapp.models.FilteredRpmTransactionTasks(
            to_install=TEST_INSTALL_PACKAGES.initdata,
            to_remove=TEST_REMOVE_PACKAGES.initdata,
            to_upgrade=TEST_UPGRADE_PACKAGES.initdata,
            modules_to_enable=TEST_ENABLE_MODULES.initdata
            )
    }
    inputs[parameter] = input_value
    created = DATADnfPluginData.create(
        dnfplugin.build_plugin_data(
            **inputs
        )
    )
    assert created.dnf_conf.platform_id == 'platform:el{}'.format(used_target_major_version)
    assert created.dnf_conf.releasever == used_target_version
    value = created
    for path in test_path:
        value = getattr(value, path)
    assert value == expected_value


def test_build_plugin_data(monkeypatch):
    monkeypatch.setattr(dnfplugin, 'get_target_version', _mocked_get_target_major_version('8.4'))
    monkeypatch.setattr(dnfplugin, 'get_target_major_version', _mocked_get_target_major_version('8'))
    # Use leapp to validate format and data
    created = DATADnfPluginData.create(
        dnfplugin.build_plugin_data(
            target_repoids=['BASEOS', 'APPSTREAM'],
            debug=True,
            test=True,
            on_aws=False,
            tasks=leapp.models.FilteredRpmTransactionTasks(
                to_install=TEST_INSTALL_PACKAGES.initdata,
                to_remove=TEST_REMOVE_PACKAGES.initdata,
                to_upgrade=TEST_UPGRADE_PACKAGES.initdata,
                modules_to_enable=TEST_ENABLE_MODULES.initdata
                )
            )
    )
    assert created.dnf_conf.debugsolver is True
    assert created.dnf_conf.test_flag is True
    assert created.rhui.aws.on_aws is False

    with pytest.raises(fields.ModelViolationError):
        DATADnfPluginData.create(
            dnfplugin.build_plugin_data(
                target_repoids=['BASEOS', 'APPSTREAM'],
                debug=True,
                test=True,
                on_aws=False,
                tasks=leapp.models.FilteredRpmTransactionTasks(
                    to_install=TEST_INSTALL_PACKAGES.initdata,
                    to_remove=TEST_REMOVE_PACKAGES.initdata,
                    to_upgrade=TEST_UPGRADE_PACKAGES.initdata,
                    # Enforcing the failure
                    modules_to_enable=(
                        leapp.models.Module(
                            name='broken', stream=None
                        ),
                    ),
                )
            )
        )


# repos/system_upgrade/common/libraries/tests/test_grub.py

import os

import pytest

from leapp.exceptions import StopActorExecution
from leapp.libraries.common import grub
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import DefaultGrub, DefaultGrubInfo

BOOT_PARTITION = '/dev/vda1'
BOOT_DEVICE = '/dev/vda'

VALID_DD = b'GRUB GeomHard DiskRead Error'
INVALID_DD = b'Nothing to see here!'

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def raise_call_error(args=None):
    raise CalledProcessError(
        message='A Leapp Command Error occured.',
        command=args,
        result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
    )


class RunMocked(object):

    def __init__(self, raise_err=False):
        self.called = 0
        self.args = None
        self.raise_err = raise_err

    def __call__(self, args, encoding=None):
        self.called += 1
        self.args = args
        if self.raise_err:
            raise_call_error(args)

        if self.args == ['grub2-probe', '--target=device', '/boot']:
            stdout = BOOT_PARTITION

        elif self.args == ['lsblk', '-spnlo', 'name', BOOT_PARTITION]:
            stdout = BOOT_DEVICE

        return {'stdout': stdout}


def open_mocked(fn, flags):
    return open(
        os.path.join(CUR_DIR, 'grub_valid') if fn == BOOT_DEVICE else os.path.join(CUR_DIR, 'grub_invalid'), 'r'
    )


def open_invalid(fn, flags):
    return open(os.path.join(CUR_DIR, 'grub_invalid'), 'r')


def read_mocked(f, size):
    return f.read(size)


def close_mocked(f):
    f.close()


def test_get_grub_device_library(monkeypatch):
    run_mocked = RunMocked()
    monkeypatch.setattr(grub, 'run', run_mocked)
    monkeypatch.setattr(os, 'open', open_mocked)
    monkeypatch.setattr(os, 'read', read_mocked)
    monkeypatch.setattr(os, 'close', close_mocked)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    result = grub.get_grub_device()
    assert grub.run.called == 2
    assert BOOT_DEVICE == result
    assert not api.current_logger.warnmsg
    assert 'GRUB is installed on {}'.format(result) in api.current_logger.infomsg


def test_get_grub_device_fail_library(monkeypatch):
    run_mocked = RunMocked(raise_err=True)
    monkeypatch.setattr(grub, 'run', run_mocked)
    monkeypatch.setattr(os, 'open', open_mocked)
    monkeypatch.setattr(os, 'read', read_mocked)
    monkeypatch.setattr(os, 'close', close_mocked)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    with pytest.raises(StopActorExecution):
        grub.get_grub_device()
    assert grub.run.called == 1
    err = 'Could not get name of underlying /boot partition'
    assert err in api.current_logger.warnmsg


def test_device_no_grub_library(monkeypatch):
    run_mocked = RunMocked()
    monkeypatch.setattr(grub, 'run', run_mocked)
    monkeypatch.setattr(os, 'open', open_invalid)
    monkeypatch.setattr(os, 'read', read_mocked)
    monkeypatch.setattr(os, 'close', close_mocked)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    result = grub.get_grub_device()
    assert grub.run.called == 2
    assert not result


@pytest.mark.parametrize('enabled', [True, False])
def test_is_blscfg_library(monkeypatch, enabled):
    bls_cfg_enabled = DefaultGrubInfo(
        default_grub_info=[DefaultGrub(name='GRUB_ENABLE_BLSCFG', value='true')]
    )

    bls_cfg_not_enabled = DefaultGrubInfo(
        default_grub_info=[DefaultGrub(name='GRUB_ENABLE_BLSCFG', value='false')]
    )

    bls_cfg = bls_cfg_enabled if enabled else bls_cfg_not_enabled

    result = grub.is_blscfg_enabled_in_defaultgrub(bls_cfg)
    if enabled:
        assert result
    else:
        assert not result


# repos/system_upgrade/common/libraries/tests/test_persistentnetnames_library.py

from leapp.libraries.common import persistentnetnames
from leapp.libraries.common.testutils import produce_mocked
from leapp.libraries.stdlib import api


class AttributesTest(object):
    def __init__(self):
        self.attributes = {
            'address': b'fa:16:3e:cd:26:5a'
        }

    def get(self, attribute):
        if attribute in self.attributes:
            return self.attributes[attribute]
        raise KeyError


class DeviceTest(object):
    def __init__(self):
        self.dict_data = {
             'ID_NET_DRIVER': 'virtio_net',
             'ID_VENDOR_ID': '0x1af4',
             'ID_PATH': 'pci-0000:00:03.0',
        }

    def __getitem__(self, key):
        if key in self.dict_data:
            return self.dict_data[key]
        raise KeyError

    @property
    def sys_name(self):
        return 'eth'

    @property
    def device_path(self):
        return '/devices/pci0000:00/0000:00:03.0/virtio0/net/eth0'

    @property
    def attributes(self):
        return AttributesTest()


def provide_test_interfaces():
    return [DeviceTest()]


def test_getting_interfaces(monkeypatch):
    monkeypatch.setattr(persistentnetnames, 'physical_interfaces', provide_test_interfaces)
    monkeypatch.setattr(api, 'produce', produce_mocked())
    interface = next(persistentnetnames.interfaces())
    assert interface.name
    assert interface.devpath
    assert interface.driver
    assert interface.vendor
    assert interface.pci_info
    assert interface.mac


# repos/system_upgrade/common/libraries/tests/test_repofileutils.py

import json
import os

from leapp.libraries.common import repofileutils

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def test_invert_dict():
    input_dict = {1: ['a', 'b'], 2: ['b'], 3: []}
    inv_dict = repofileutils._invert_dict(input_dict)
    assert inv_dict == {'a': [1], 'b': [1, 2]}


def test_parse_repofile():
    repofile = repofileutils.parse_repofile(os.path.join(CUR_DIR, 'sample_repos.txt'))

    repo_appstream = [repo for repo in repofile.data if repo.repoid == 'AppStream'][0]
    assert repo_appstream.name == 'CentOS-$releasever - AppStream'
    assert repo_appstream.baseurl is None  # comments shouldn't get parsed
    assert repo_appstream.metalink is None
    assert repo_appstream.mirrorlist == ('http://mirrorlist.centos.org/?release=$releasever'
                                         '&arch=$basearch&repo=AppStream&infra=$infra')
    assert repo_appstream.enabled is True
    additional_appstream = json.loads(repo_appstream.additional_fields)
    assert additional_appstream['gpgcheck'] == '1'
    assert additional_appstream['gpgkey'] == 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial'
    assert additional_appstream['cost'] == '77'
    assert additional_appstream.get('baseurl') is None

    repo_leapp = [repo for repo in repofile.data if repo.repoid == 'leapp-copr'][0]
    assert repo_leapp.name == 'Copr repo for devel Leapp builds'
    assert repo_leapp.baseurl == 'http://coprbe.devel.redhat.com/results/oam-group/leapp/rhel-7-x86_64/'
    assert repo_leapp.metalink is None
    assert repo_leapp.mirrorlist is None
    assert repo_leapp.enabled is False
    additional_leapp = json.loads(repo_leapp.additional_fields)
    assert additional_leapp['type'] == 'rpm-md'
    assert additional_leapp['skip_if_unavailable'] == 'True'
    assert additional_leapp['gpgcheck'] == '0'
    assert additional_leapp['gpgkey'] == 'http://coprbe.devel.redhat.com/results/oam-group/leapp/pubkey.gpg'
    assert additional_leapp['repo_gpgcheck'] == '0'
    assert additional_leapp['enabled_metadata'] == '1'
    assert len(additional_leapp) == 6

    assert len([repo for repo in repofile.data if repo.repoid == 'spe-ci_al.cha:rs']) == 1

    repos_duplicate = [repo for repo in repofile.data if repo.repoid == 'duplicate']
    assert len(repos_duplicate) == 1  # only one instance got through
    assert repos_duplicate[0].name == 'Duplicate 2'  # and it's the latter one


# repos/system_upgrade/common/libraries/tests/test_rhsm.py

from collections import namedtuple
import os

import pytest

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import repofileutils, rhsm
from leapp.libraries.common.testutils import (
    create_report_mocked,
    CurrentActorMocked,
    logger_mocked
)
from leapp.libraries.stdlib import CalledProcessError, api
from leapp.models import RepositoryFile, RepositoryData

Repository = namedtuple('Repository', ['repoid', 'file'])
LIST_SEPARATOR = '\n    - '

# External commands called by the RHSM library
CMD_RHSM_LIST_CONSUMED = ('subscription-manager', 'list', '--consumed')
CMD_RHSM_STATUS = ('subscription-manager', 'status')
CMD_RHSM_RELEASE = ('subscription-manager', 'release')
CMD_RHSM_LIST_ENABLED_REPOS = ('subscription-manager', 'repos', '--list-enabled')

RHSM_STATUS_OUTPUT_NOSCA = '''
+-------------------------------------------+
   System Status Details
+-------------------------------------------+
Overall Status: Current

System Purpose Status: Not Specified
'''

RHSM_STATUS_OUTPUT_SCA = '''
+-------------------------------------------+
   System Status Details
+-------------------------------------------+
Overall Status: Current

System Purpose Status: Matched

Content Access Mode is set to Simple Content Access
'''

# Used to simulate realistic output of RHSM, therefore carries more information than `Repository` namedtuple
RHSMRepositoryEntry = namedtuple('RHSMRepositoryEntry', ('id', 'name', 'url', 'enabled'))  # For clarity purposes
RHSM_ENABLED_REPOS = [
    RHSMRepositoryEntry(
        id='rhel-8-for-x86_64-appstream-rpms',
        name='Appstream',
        url='some_url',
        enabled='1'),
    RHSMRepositoryEntry(
        id='satellite-tools-6.6-for-rhel-8-x86_64-rpms',
        name='Satellite',
        url='some_url',
        enabled='1'),
    RHSMRepositoryEntry(
        id='rhel-8-for-x86_64-baseos-rpms',
        name='Base',
        url='some_url',
        enabled='1')
]


class IsolatedActionsMocked(object):
    def __init__(self, call_stdout=None, raise_err=False):
        self.commands_called = []
        self.call_return = {'stdout': call_stdout, 'stderr': None}
        self.raise_err = raise_err

        # A map from called commands to their mocked output
        self.mocked_command_call_outputs = dict()

    def call(self, cmd, *args, **dummy_kwargs):
        self.commands_called.append(cmd)
        if self.raise_err:
            raise_call_error(cmd)

        return self.mocked_command_call_outputs.get(
            tuple(cmd),  # Cast to tuple, as list is not hashable
            self.call_return)

    def add_mocked_command_call_with_stdout(self, cmd, stdout):
        # We cast `cmd` from list to tuple, as a list cannot be hashed
        self.mocked_command_call_outputs[tuple(cmd)] = {
            'stdout': stdout,
            'stderr': None}

    def full_path(self, path):
        return path


@pytest.fixture
def actor_mocked(monkeypatch):
    """
    Fixture providing a mocked actor that was already used to monkeypatch api.current_actor.

    Introduced to reduce repetition inside tests.
    """
    actor = CurrentActorMocked()
    monkeypatch.setattr(api, 'current_actor', actor)
    return actor


@pytest.fixture
def context_mocked():
    return IsolatedActionsMocked()


def raise_call_error(args=None, exit_code=1):
    raise CalledProcessError(
        message='Command {0} failed with exit code {1}.'.format(str(args), exit_code),
        command=args,
        result={'signal': None, 'exit_code': exit_code, 'pid': 0, 'stdout': 'fake out', 'stderr': 'fake err'}
    )


def _gen_repo(repoid):
    return RepositoryData(repoid=repoid, name='name {}'.format(repoid))


def _gen_repofile(rfile, data=None):
    if data is None:
        data = [_gen_repo("{}-{}".format(rfile.split("/")[-1], i)) for i in range(3)]
    return RepositoryFile(file=rfile, data=data)


@pytest.mark.parametrize('other_repofiles', [
    [],
    [_gen_repofile("foo")],
    [_gen_repofile("foo"), _gen_repofile("bar")],
])
@pytest.mark.parametrize('rhsm_repofile', [
    None,
    _gen_repofile(rhsm._DEFAULT_RHSM_REPOFILE, []),
    _gen_repofile(rhsm._DEFAULT_RHSM_REPOFILE, [_gen_repo("rh-0")]),
    _gen_repofile(rhsm._DEFAULT_RHSM_REPOFILE),
])
def test_get_available_repo_ids(monkeypatch, other_repofiles, rhsm_repofile):
    context_mocked = IsolatedActionsMocked()
    repos = other_repofiles[:]
    if rhsm_repofile:
        repos.append(rhsm_repofile)
    rhsm_repos = [repo.repoid for repo in rhsm_repofile.data] if rhsm_repofile else []

    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(rhsm, '_inhibit_on_duplicate_repos', lambda x: None)
    monkeypatch.setattr(repofileutils, 'get_parsed_repofiles', lambda x: repos)

    result = rhsm.get_available_repo_ids(context_mocked)

    rhsm_repos.sort()
    assert context_mocked.commands_called == [['yum', 'clean', 'all']]
    assert result == rhsm_repos
    if result:
        msg = (
            'The following repoids are available through RHSM:{0}{1}'
            .format(LIST_SEPARATOR, LIST_SEPARATOR.join(rhsm_repos))
        )
        assert msg in api.current_logger.infomsg
    else:
        assert 'There are no repos available through RHSM.' in api.current_logger.infomsg


def test_get_available_repo_ids_error():
    context_mocked = IsolatedActionsMocked(raise_err=True)

    with pytest.raises(StopActorExecutionError) as err:
        rhsm.get_available_repo_ids(context_mocked)

    assert 'Unable to use yum' in str(err)


def test_inhibit_on_duplicate_repos(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    repofiles = [
        _gen_repofile("foo", [_gen_repo('repoX'), _gen_repo('repoY')]),
        _gen_repofile("bar", [_gen_repo('repoX')]),
    ]

    rhsm._inhibit_on_duplicate_repos(repofiles)

    dups = ['repoX']
    assert ('The following repoids are defined multiple times:{0}{1}'
            .format(LIST_SEPARATOR, LIST_SEPARATOR.join(dups))) in api.current_logger.warnmsg
    assert reporting.create_report.called == 1
    assert 'inhibitor' in reporting.create_report.report_fields['flags']
    assert reporting.create_report.report_fields['title'] == 'A YUM/DNF repository defined multiple times'
    summary = ('The following repositories are defined multiple times:{0}{1}'
               .format(LIST_SEPARATOR, LIST_SEPARATOR.join(dups)))
    assert summary in reporting.create_report.report_fields['summary']


def test_inhibit_on_duplicate_repos_no_dups(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    rhsm._inhibit_on_duplicate_repos([_gen_repofile("foo")])

    assert not api.current_logger.warnmsg
    assert reporting.create_report.called == 0


def test_sku_listing(monkeypatch, actor_mocked, context_mocked):
    """Tests whether the rhsm library can obtain used SKUs correctly."""
    context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_LIST_CONSUMED, 'SKU: 598339696910')

    attached_skus = rhsm.get_attached_skus(context_mocked)

    assert_fail_description = 'Some calls to subscription-manager were expected.'
    assert context_mocked.commands_called, assert_fail_description

    assert_fail_description = 'RHSM command reported 1 SKU, however {0} were detected.'.format(
        len(attached_skus)
    )
    assert len(attached_skus) == 1, assert_fail_description

    assert_fail_description = 'The parsed SKU is different than the one contained in the mocked RHSM output.'
    assert attached_skus[0] == '598339696910', assert_fail_description


def test_scanrhsminfo_with_skip_rhsm(monkeypatch, context_mocked):
    """Tests whether the scan_rhsm_info respects the LEAPP_NO_RHSM environmental variable."""
    mocked_actor = CurrentActorMocked(envars={'LEAPP_NO_RHSM': '1'})
    monkeypatch.setattr(api, 'current_actor', mocked_actor)

    result = rhsm.scan_rhsm_info(context_mocked)

    assert_fail_description = 'No external shell commands should be executed when RHSM is skipped.'
    assert not context_mocked.commands_called, assert_fail_description

    assert result is None, 'The `scan_rhsm_info` should not provide any output when RHSM is skipped.'


def test_get_release(monkeypatch, actor_mocked, context_mocked):
    """Tests whether the library correctly retrieves release from RHSM."""
    context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_RELEASE, 'Release: 7.9')

    release = rhsm.get_release(context_mocked)

    assert release, 'No release information detected (but valid release info was provided).'
    assert release == '7.9', 'Detected release is incorrect.'


def test_get_release_with_release_not_set(monkeypatch, actor_mocked, context_mocked):
    """Tests whether the library does not retrieve release information when the release is not set."""
    # Test whether no realease is detected correctly too
    context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_RELEASE, 'Release not set')

    release = rhsm.get_release(context_mocked)

    fail_description = 'The release information was obtained, even if "No release set" was repored by rhsm.'
    assert not release, fail_description


def test_is_manifest_sca_on_nonsca_system(monkeypatch, actor_mocked, context_mocked):
    """Tests whether the library obtains the SCA information correctly from a non-SCA system."""
    context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_STATUS, RHSM_STATUS_OUTPUT_NOSCA)

    is_sca = rhsm.is_manifest_sca(context_mocked)
    assert not is_sca, 'SCA was detected on a non-SCA system.'


def test_is_manifest_sca_on_sca_system(monkeypatch, actor_mocked, context_mocked):
    """Tests whether the library obtains the SCA information from SCA system correctly."""
    context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_STATUS, RHSM_STATUS_OUTPUT_SCA)

    is_sca = rhsm.is_manifest_sca(context_mocked)
    assert is_sca, 'Failed to detected SCA on a SCA system.'


def test_get_enabled_repo_ids(monkeypatch, actor_mocked, context_mocked):
    """Tests whether the library retrieves correct information about enabled repositories."""
    # Prepare the (realistic) RHSM output
    rhsm_list_enabled_output = '''
    +----------------------------------------------------------+
       Available Repositories in /etc/yum.repos.d/redhat.repo
    +----------------------------------------------------------+
    '''

    for enabled_repository in RHSM_ENABLED_REPOS:
        rhsm_output_fragment = 'Repo ID: {0}\n'.format(enabled_repository.id)
        rhsm_output_fragment += 'Repo Name: {0}\n'.format(enabled_repository.name)
        rhsm_output_fragment += 'Repo URL: {0}\n'.format(enabled_repository.url)
        rhsm_output_fragment += 'Enabled: {0}\n'.format(enabled_repository.enabled)
        rhsm_output_fragment += '\n'
        rhsm_list_enabled_output += rhsm_output_fragment

    context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_LIST_ENABLED_REPOS, rhsm_list_enabled_output)

    enabled_repo_ids = rhsm.get_enabled_repo_ids(context_mocked)

    fail_description = 'Failed to detected enabled repositories on the system.'
    assert len(enabled_repo_ids) == 3, fail_description

    fail_description = 'Failed to retrieve repository ID provided in the RHSM output.'
    for enabled_repository in RHSM_ENABLED_REPOS:
        assert enabled_repository.id in enabled_repo_ids, fail_description


def test_get_existing_product_certificates(monkeypatch, actor_mocked, context_mocked):
    """Verifies that the library is able to correctly retrieve existing product certificates."""

    CERT_DIRS_LAYOUT = {
        '/etc/pki/product': ['cert1', 'cert2'],
        '/etc/pki/product-default': ['cert3']
    }

    def mocked_isdir(path):
        if path in CERT_DIRS_LAYOUT:
            return True
        err_message = 'RHSM library should not gather info about additional dirs (attempted to isdir: {0}).'
        raise ValueError(err_message.format(path))

    def mocked_listdir(path):
        if path in CERT_DIRS_LAYOUT:
            return CERT_DIRS_LAYOUT[path]
        err_message = 'RHSM library should not listdir additional dirs (attempted to listdir: {0}).'
        raise ValueError(err_message.format(path))

    def mocked_isfile(path):
        if path in CERT_DIRS_LAYOUT:
            # The certificate directories are not files
            return False

        basename = os.path.basename(path)
        dirname = os.path.dirname(path)
        if dirname in CERT_DIRS_LAYOUT:
            return basename in CERT_DIRS_LAYOUT[dirname]

        err_message = 'RHSM library should not isfile additional paths (attempted to isfile: {0}).'
        raise ValueError(err_message.format(path))

    monkeypatch.setattr(rhsm.os.path, 'isdir', mocked_isdir)
    monkeypatch.setattr(rhsm.os, 'listdir', mocked_listdir)
    monkeypatch.setattr(rhsm.os.path, 'isfile', mocked_isfile)

    existing_product_certificates = rhsm.get_existing_product_certificates(context_mocked)

    fail_description = 'Retrieved different number of certificates than expected.'
    assert len(existing_product_certificates) == 3, fail_description

    fail_description_bad_dir = 'Found certificate in unexpected path: {0}'
    fail_description_bad_cert_file = 'Found certificate file that was not provided by mocked output: {0}'
    for certificate_path in existing_product_certificates:
        dirname = os.path.dirname(certificate_path)
        basename = os.path.basename(certificate_path)
        assert dirname in CERT_DIRS_LAYOUT, fail_description_bad_dir.format(certificate_path)
        assert basename in CERT_DIRS_LAYOUT[dirname], fail_description_bad_cert_file.format(certificate_path)


def test_get_existing_product_certificates_missing_cert_directory(monkeypatch, actor_mocked, context_mocked):
    """Tests whether the library is able to retrieve certificates even if /etc/pki/product is missing."""

    def mocked_isdir(path):
        if path == '/etc/pki/product':
            return False  # Directory is missing
        if path == '/etc/pki/product-default':
            return True

        err_msg = 'Tried to isdir a path that is not a part of the mocked paths. Path: {0}'
        raise ValueError(err_msg.format(path))

    def mocked_isfile(path):
        if path == '/etc/pki/product-default/cert':
            return True

        err_msg = 'Tried to use isfile on a path that is not a part of the mocked paths. Path: {0}'
        raise ValueError(err_msg.format(path))

    def mocked_listdir(path):
        if path == '/etc/pki/product-default':
            return ['cert']

        err_msg = 'Tried to use listdir on a path that is not a part of the mocked paths. Path: {0}'
        raise ValueError(err_msg.format(path))

    monkeypatch.setattr(rhsm.os.path, 'isdir', mocked_isdir)
    monkeypatch.setattr(rhsm.os, 'listdir', mocked_listdir)
    monkeypatch.setattr(rhsm.os.path, 'isfile', mocked_isfile)

    existing_product_certificates = rhsm.get_existing_product_certificates(context_mocked)

    fail_description = 'Library identified more certificates than there are in mocked outputs.'
    assert len(existing_product_certificates) == 1, fail_description
    fail_description = 'Library failed to identify certificate from mocked outputs.'
    assert existing_product_certificates[0] == '/etc/pki/product-default/cert', fail_description


# repos/system_upgrade/common/libraries/tests/test_testutils.py

import errno

from leapp.libraries.common.testutils import make_IOError, make_OSError


def test_make_IOError():
    exception = make_IOError(errno.ENOENT)
    assert isinstance(exception, IOError)
    assert exception.errno == errno.ENOENT

    exception = make_IOError(errno.ENOTDIR)
    assert isinstance(exception, IOError)
    assert exception.errno == errno.ENOTDIR


def test_make_OSError():
    exception = make_OSError(errno.ENOENT)
    assert isinstance(exception, OSError)
    assert exception.errno == errno.ENOENT

    exception = make_OSError(errno.ENOTDIR)
    assert isinstance(exception, OSError)
    assert exception.errno == errno.ENOTDIR


# repos/system_upgrade/common/libraries/tests/test_utils_logging_handler.py

# -*- coding: UTF-8 -*-
import os.path
import sys

from leapp.libraries.common.utils import config, logging_handler
from leapp.libraries.stdlib.call import STDERR, STDOUT


def test_logging_handler(capfdbinary, monkeypatch):
    debug_on = False
    monkeypatch.setattr(config, 'is_debug', lambda: debug_on)
    panagrams_path = os.path.join(os.path.dirname(__file__), 'panagrams')
    with open(panagrams_path, 'rb') as f:
        test_data = f.read()

    bin_test_data = test_data
    if isinstance(test_data, str) and str is not bytes:
        bin_test_data = test_data.encode()
    # Should not log anything
    for c in range(0, len(bin_test_data)):
        logging_handler((None, STDERR), bin_test_data[c:c+1])
    debug_on = True
    for c in range(0, len(bin_test_data)):
        logging_handler((None, STDERR), bin_test_data[c:c+1])
    for c in range(0, len(bin_test_data)):
        logging_handler((None, STDOUT), bin_test_data[c:c+1])
    captured = capfdbinary.readouterr()
    assert captured.out == bin_test_data
    assert captured.err == bin_test_data


def test_logging_handler_arrow(capfdbinary, monkeypatch):
    debug_on = False
    monkeypatch.setattr(config, 'is_debug', lambda: debug_on)
    # → => '\xe2\x86\x92'
    if sys.version_info > (3, 0):
        buf = b'\xe2\x86\x92'
    else:
        buf = '\xe2\x86\x92'

    for i in range(0, len(buf)):
        debug_on = False
        logging_handler((None, STDERR), buf[i:i+1])
        debug_on = True
        logging_handler((None, STDERR), buf[i:i+1])
        logging_handler((None, STDOUT), buf[i:i+1])
    captured = capfdbinary.readouterr()
    assert captured.err == buf
    assert captured.out == buf


# repos/system_upgrade/el8toel9/actors/checkblsgrubcfgonppc64/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import blsgrubcfgonppc64
from leapp.models import DefaultGrubInfo, FirmwareFacts, GrubCfgBios, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckBlsGrubOnPpc64(Actor):
    """
    Check whether GRUB config is BLS aware on RHEL 8 ppc64le systems

    After a ppc64 system is upgraded from RHEL 8 to RHEL 9 and
    GRUB config on RHEL 8 is not yet BLS aware, the system boots
    into el8 kernel because the config is not successfuly migrated by
    GRUB during the upgrade process.

    IMPORTANT NOTE: The later fix which is based on the outcome of this
    actor is applied only for virtualized ppc64le systems as we got
    unexpected behavior on bare metal ppc64le systems which needs to be
    investigated first.

    """

    name = 'check_bls_grub_onppc64'
    consumes = (DefaultGrubInfo, GrubCfgBios, FirmwareFacts)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        blsgrubcfgonppc64.process()


# repos/system_upgrade/el8toel9/actors/checkcpu/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import cpu
from leapp.models import CPUInfo, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckCPU(Actor):
    """
    Check whether the CPU is supported by the target system. Inhibit upgrade if not.

    Currently we know just about cases with s390x where the set of CPUs supported
    by RHEL 9 is subset of CPUs supported on RHEL 8. We can detect such cases based
    on the machine field inside the /proc/cpuinfo file. expected values of the
    field on supported machines are: 3906, 3907, 8561, 8562.
    """

    name = "checkcpu"
    consumes = (CPUInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag,)

    def process(self):
        cpu.process()


# repos/system_upgrade/el8toel9/actors/checkcustomnetworkscripts/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import customnetworkscripts
from leapp.models import Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class CheckCustomNetworkScripts(Actor):
    """
    Check the existence of custom network-scripts and warn user about possible
    manual intervention requirements.
    """

    name = "check_custom_network_scripts"
    produces = (Report,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        customnetworkscripts.process()


# repos/system_upgrade/el8toel9/actors/checkifcfg/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkifcfg_ifcfg as ifcfg
from leapp.models import InstalledRPM, Report, RpmTransactionTasks
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class CheckIfcfg(Actor):
    """
    Ensures that ifcfg files are compatible with NetworkManager

    Checks whether the ifcfg files would work with NetworkManager's ifcfg-rh
    compatibility module -- they are of known type, well-formed and not
    explicitly disabled with NM_CONTROLLED=no.

    Makes sure relevant NetworkManager modules end up getting installed.
    """

    name = "check_ifcfg"
    consumes = (InstalledRPM,)
    produces = (Report, RpmTransactionTasks,)
    tags = (IPUWorkflowTag, FactsPhaseTag,)

    def process(self):
        ifcfg.process()


# repos/system_upgrade/el8toel9/actors/checkvdo/actor.py

from leapp.actors import Actor
from leapp.dialogs import Dialog
from leapp.dialogs.components import BooleanComponent
from leapp.libraries.actor.checkvdo import check_vdo
from leapp.models import Report, VdoConversionInfo
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckVdo(Actor):
    """
    Check if VDO devices need to be migrated to lvm management.

    `Background`
    ============
    In RHEL 9.0 the indepdent VDO management software, `vdo manager`, is
    superseded by LVM management.  Existing VDOs must be converted to LVM-based
    management *before* upgrading to RHEL 9.0.

    The `CheckVdo` actor provides a pre-upgrade check for VDO devices that have
    not been converted and, if any are found, produces an inhibitory report
    during upgrade check phase.   If none are found `CheckVdo` does not produce
    a report concerning such VDO devices.

    As there currently exists the theoretical possibility that a VDO device may
    not complete its conversion to LVM-based management (e.g., via a poorly
    timed system crash during the conversion) `CheckVdo` also provides a
    pre-upgrade check for VDO devices in this state.  If any are found
    `CheckVdo` produces an inhibitory report during upgrade check phase.  If
    none are found `CheckVdo` does not produce a report concerning such VDO
    devices.

    If the VdoConversionInfo model indicates unexpected errors occurred during
    scanning CheckVdo will produce appropriate inhibitory reports.

    Lastly, if the VdoConversionInfo model indicates conditions exist where VDO
    devices could exist but the necessary software to check was not installed
    on the system CheckVdo will present a dialog to the user. This dialog will
    ask the user to either install the required software if the user knows or
    is unsure that VDO devices exist or to approve the continuation of the
    upgrade if the user is certain that no VDO devices exist.
    """

    name = 'check_vdo'
    consumes = (VdoConversionInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)
    dialogs = (
        Dialog(
            scope='check_vdo',
            reason='Confirmation',
            components=(
                BooleanComponent(
                    key='no_vdo_devices',
                    label='The system has no VDO devices?',
                    description='If the system has no VDO devices it is safe '
                                'to continute the upgrade.  If there are VDO '
                                'devices they must all be converted to LVM '
                                'management before the upgrade can proceed.',
                    reason='Based on installed packages it is possible that '
                           'VDO devices exist on the system.  All VDO devices '
                           'must be converted to being managed by LVM before '
                           'the upgrade occurs.  Because the \'vdo\' package '
                           'is not installed it cannot be determined whether '
                           'any VDO devices exist that have not yet been '
                           'converted.  If the devices are not converted and '
                           'upgrade proceeds the data on unconverted VDO '
                           'devices will be inaccessible.  If you have any '
                           'doubts you should choose to install the \'vdo\' '
                           'package and re-run the upgrade process to check '
                           'for unconverted VDO devices.  If you are certain '
                           'that the system has no VDO devices or that all '
                           'VDO devices have been converted to LVM management '
                           'you may opt to allow upgrade to proceed.'
                ),
            )
        ),
    )

    def get_no_vdo_devices_response(self):
        return self.get_answers(self.dialogs[0]).get('no_vdo_devices')

    def process(self):
        for conversion_info in self.consume(VdoConversionInfo):
            check_vdo(conversion_info)


# repos/system_upgrade/el8toel9/actors/firewalldcheckallowzonedrifting/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.models import FirewalldGlobalConfig, FirewallsFacts
from leapp.reporting import create_report, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class FirewalldCheckAllowZoneDrifting(Actor):
    """
    This actor will check if AllowZoneDrifiting=yes in firewalld.conf. This
    option has been removed in RHEL-9 and behavior is as if
    AllowZoneDrifiting=no.
    """

    name = 'firewalld_check_allow_zone_drifting'
    consumes = (FirewallsFacts, FirewalldGlobalConfig)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        # If firewalld is not enabled then don't bother the user about its
        # configuration. This Report keys off a _default_ value and as such
        # will trigger for all users that have not done one of the following:
        #   - disabled firewalld
        #   - manually set AllowZoneDrifting=no (as firewalld logs suggests)
        #
        for facts in self.consume(FirewallsFacts):
            if not facts.firewalld.enabled:
                return

        for facts in self.consume(FirewalldGlobalConfig):
            if not facts.allowzonedrifting:
                return

        create_report([
            reporting.Title('Firewalld Configuration AllowZoneDrifting Is Unsupported'),
            reporting.Summary('Firewalld has enabled configuration option '
                              '"{conf_key}" which has been removed in RHEL-9. '
                              'New behavior is as if "{conf_key}" was set to "no".'.format(
                                  conf_key='AllowZoneDrifiting')),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SANITY, reporting.Tags.FIREWALL]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.ExternalLink(
                url='https://access.redhat.com/articles/4855631',
                title='Changes in firewalld related to Zone Drifting'),
            reporting.Remediation(
                hint='Set AllowZoneDrifting=no in /etc/firewalld/firewalld.conf',
                commands=[['sed -i "s/^AllowZoneDrifting=.*/AllowZoneDrifting=no/" '
                           '/etc/firewalld/firewalld.conf']]),
        ])


# repos/system_upgrade/el8toel9/actors/firewalldcheckservicetftpclient/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.models import FirewalldUsedObjectNames
from leapp.reporting import create_report, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class FirewalldCheckServiceTftpClient(Actor):
    """
    This actor will inhibit if firewalld's configuration is using service
    'tftp-client'.
    """

    name = 'firewalld_check_service_tftp_client'
    consumes = (FirewalldUsedObjectNames,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        tftp_client_service = 'tftp-client'
        send_report = False

        for facts in self.consume(FirewalldUsedObjectNames):
            if tftp_client_service in facts.services:
                send_report = True

        if send_report:
            create_report([
                reporting.Title('Firewalld Service tftp-client Is Unsupported'),
                reporting.Summary('Firewalld has service "{service}" enabled. '
                                  'Service "{service}" has been removed in RHEL-9.'.format(
                                      service=tftp_client_service)),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags([reporting.Tags.SANITY, reporting.Tags.FIREWALL]),
                reporting.Flags([reporting.Flags.INHIBITOR]),
                reporting.Remediation(
                    hint=(
                        'Remove all usage of service "{service}" from '
                        'firewalld\'s permanent configuration. '
                        'It may be in use by: zones, policies, or rich rules.\n'
                        'Usage can be found by listing zone and policy '
                        'configuration:\n'
                        '  # firewall-cmd --permanent --list-all-zones\n'
                        '  # firewall-cmd --permanent --list-all-policies\n'
                        'Example to remove usage from a zone:\n'
                        '  # firewall-cmd --permanent --zone public '
                        ' --remove-service {service}\n'.format(
                            service=tftp_client_service)
                        )
                    ),
            ])


# repos/system_upgrade/el8toel9/actors/firewalldcollectglobalconfig/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.private_firewalldcollectglobalconfig import read_config
from leapp.models import FirewalldGlobalConfig
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class FirewalldCollectGlobalConfig(Actor):
    """
    This actor reads firewalld's configuration and produces Model
    FirewalldGlobalConfig.
    """

    name = 'firewalld_collect_global_config'
    consumes = ()
    produces = (FirewalldGlobalConfig,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        self.produce(read_config())


# repos/system_upgrade/el8toel9/actors/firewalldcollectusedobjectnames/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.private_firewalldcollectusedobjectnames import read_config
from leapp.models import FirewalldUsedObjectNames
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class FirewalldCollectUsedObjectNames(Actor):
    """
    This actor reads firewalld's configuration and produces Model
    FirewalldUsedObjectNames.
    """

    name = 'firewalld_collect_used_object_names'
    consumes = ()
    produces = (FirewalldUsedObjectNames,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        self.produce(read_config())


# repos/system_upgrade/el8toel9/actors/grub2mkconfigonppc64/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import grub2mkconfigonppc64
from leapp.models import DefaultGrubInfo, FirmwareFacts
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class Grub2MkconfigOnPpc64(Actor):
    """
    Regenerate Grub config after RHEL 8 to RHEL 9 rpm transaction

    Actor runs 'grub2-mkconfig' to regenerate Grub config after RHEL 8 to RHEL 9
    rpm transaction in case system is running on ppc64le, GRUB_ENABLE_BLSCFG=true
    and '/boot/grub2/grub.cfg' config file does not yet work with BLS entries.
    For the system to successfully boot into el9 kernel, it is essential that the
    grub config file is abvle to work with the BLS entries.

    IMPORTANT NOTE: The fix is applied only for virtualized ppc64le systems as we got
    unexpected behavior on bare metal ppc64le systems which needs to be
    investigated first.

    """

    name = 'grub2mkconfig_on_ppc64'
    consumes = (DefaultGrubInfo, FirmwareFacts)
    produces = ()
    tags = (IPUWorkflowTag, ApplicationsPhaseTag)

    def process(self):
        grub2mkconfigonppc64.process()


# repos/system_upgrade/el8toel9/actors/mariadbcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.mariadbcheck import report_installed_packages
from leapp.models import InstalledRedHatSignedRPM, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class MariadbCheck(Actor):
    """
    Actor checking for presence of MariaDB installation.

    Provides user with information related to upgrading systems
    with MariaDB installed.
    """
    name = 'mariadb_check'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        report_installed_packages()


# repos/system_upgrade/el8toel9/actors/networkdeprecations/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import networkdeprecations
from leapp.models import Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class CheckNetworkDeprecations(Actor):
    """
    Ensures that network configuration doesn't rely on unsupported settings

    Inhibits upgrade if the network configuration would not work with
    NetworkManager on the upgraded system due functionality being deprecated.

    Includes check for insecure wireless network encryption configuration
    what will not work with RHEL9.
    """

    name = "network_deprecations"
    produces = (Report,)
    tags = (IPUWorkflowTag, FactsPhaseTag,)

    def process(self):
        networkdeprecations.process()


# repos/system_upgrade/el8toel9/actors/postgresqlcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.postgresqlcheck import report_installed_packages
from leapp.models import InstalledRedHatSignedRPM, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class PostgresqlCheck(Actor):
    """
    Actor checking for presence of PostgreSQL installation.

    Provides user with information related to upgrading systems
    with PostgreSQL installed.
    """
    name = 'postgresql_check'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        report_installed_packages()


# repos/system_upgrade/el8toel9/actors/pythonthreetmpworkaround/actor.py

import os

from leapp.actors import Actor
from leapp.libraries.stdlib import CalledProcessError, api, run
from leapp.models import TransactionCompleted
from leapp.tags import RPMUpgradePhaseTag, IPUWorkflowTag
from leapp.exceptions import StopActorExecutionError


class PythonThreeTmpWorkaround(Actor):
    """
    Create the /usr/bin/python3 alternative if not exists.

    During the RPM upgrade the /usr/bin/python3 is removed because of problem
    in alternatives. The fix requires new builds of python36 on RHEL8, python3
    on RHEL 9 ans alternatives on both systems. Once the internal repositories
    are updated, we can drop this. If the /usr/bin/python3 file exists,
    do nothing.
    """

    name = 'pythonthreetmpworkaround'
    consumes = (TransactionCompleted,)
    produces = ()
    tags = (RPMUpgradePhaseTag, IPUWorkflowTag)

    def process(self):
        if os.path.isfile('/usr/bin/python3'):
            api.current_logger().info('The python3 file exists. the actor can be removed probably.')
            return

        cmd = [
            'alternatives', '--install', '/usr/bin/python3', 'python3', '/usr/bin/python3.9', '1000000',
        ]

        sub_cmds = [
            ['--slave', '/usr/share/man/man1/python3.1.gz', 'python3-man', '/usr/share/man/man1/python3.9.1.gz'],
            ['--slave', '/usr/bin/pip3', 'pip3', '/usr/bin/pip3.9'],
            ['--slave', '/usr/bin/pip-3', 'pip-3', '/usr/bin/pip-3.9'],
            ['--slave', '/usr/bin/easy_install-3', 'easy_install-3', '/usr/bin/easy_install-3.9'],
            ['--slave', '/usr/bin/pydoc3', 'pydoc3', '/usr/bin/pydoc3.9'],
            ['--slave', '/usr/bin/pydoc-3', 'pydoc-3', '/usr/bin/pydoc3.9'],
            ['--slave', '/usr/bin/pyvenv-3', 'pyvenv-3', '/usr/bin/pyvenv-3.9'],
        ]

        for sub_cmd in sub_cmds:
            if os.path.exists(sub_cmd[1]):
                # some rhel 9 packages are already updated and handle
                # alternatives correctly using the --keep-foreign option
                continue
            cmd += sub_cmd

        try:
            run(cmd)
        except CalledProcessError as exc:
            raise StopActorExecutionError(
                message='Cannot create python3 alternatives; upgrade cannot be finished',
                details={'details': str(exc), 'stderr': exc.stderr},
            )


# repos/system_upgrade/el8toel9/actors/sssdcheck/actor.py

from leapp.actors import Actor
from leapp.models import SSSDConfig8to9
from leapp import reporting
from leapp.reporting import Report, create_report
from leapp.tags import IPUWorkflowTag, ChecksPhaseTag


COMMON_REPORT_TAGS = [reporting.Tags.AUTHENTICATION, reporting.Tags.SECURITY]

related = [
    reporting.RelatedResource('package', 'sssd'),
    reporting.RelatedResource('file', '/etc/sssd/sssd.conf')
]


class SSSDCheck8to9(Actor):
    """
    Check SSSD configuration for changes in RHEL9 and report them in model.

    Implicit files domain is disabled by default. This may affect local
    smartcard authentication if there is not explicit files domain created.

    If there is no files domain and smartcard authentication is enabled,
    we will notify the administrator.
    """

    name = 'sssd_check_8to9'
    consumes = (SSSDConfig8to9,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        model = next(self.consume(SSSDConfig8to9), None)
        if not model:
            return

        # enable_files_domain is set explicitly, change of default has no effect
        if model.enable_files_domain_set:
            return

        # there is explicit files domain, implicit files domain has no effect
        if model.explicit_files_domain:
            return

        # smartcard authentication is disabled, implicit files domain has no effect
        if not model.pam_cert_auth:
            return

        create_report([
            reporting.Title('SSSD implicit files domain is now disabled by default.'),
            reporting.Summary('Default value of [sssd]/enable_files_domain has '
                              'changed from true to false.'),
            reporting.Tags(COMMON_REPORT_TAGS),
            reporting.Remediation(
                hint='If you use smartcard authentication for local users, '
                     'set this option to true explicitly and call '
                     '"authselect enable-feature with-files-domain".'
            ),
            reporting.Severity(reporting.Severity.MEDIUM)
        ] + related)


# repos/system_upgrade/el8toel9/actors/sssdfacts/actor.py

from six.moves import configparser

from leapp.actors import Actor
from leapp.libraries.actor.sssdfacts8to9 import SSSDFactsLibrary
from leapp.models import SSSDConfig8to9
from leapp.tags import IPUWorkflowTag, FactsPhaseTag


class SSSDFacts8to9(Actor):
    """
    Check SSSD configuration for changes in RHEL9 and report them in model.

    Implicit files domain is disabled by default. This may affect local
    smartcard authentication if there is not explicit files domain created.

    If there is no files domain and smartcard authentication is enabled,
    we will notify the administrator.
    """

    name = 'sssd_facts_8to9'
    consumes = ()
    produces = (SSSDConfig8to9,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        try:
            config = configparser.RawConfigParser()
            config.read('/etc/sssd/sssd.conf')
        except configparser.Error:
            # SSSD is not configured properly. Nothing to do.
            self.log.warning('SSSD configuration unreadable.')
            return

        self.produce(SSSDFactsLibrary(config).process())


# repos/system_upgrade/el8toel9/actors/vdoconversionscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import vdoconversionscanner
from leapp.models import InstalledRedHatSignedRPM, StorageInfo, VdoConversionInfo
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class VdoConversionScanner(Actor):
    """
    Provides conversion info about VDO devices.

    A VdoConversionInfo message containing the data will be produced.

    In RHEL 9.0 the indepdent VDO management software, `vdo manager`, is
    superseded by LVM management.  Existing VDOs must be converted to LVM-based
    management *before* upgrading to RHEL 9.0.

    The `VdoConversionScanner` actor provides a pre-upgrade check for VDO
    devices.  Consuming the StorageInfo model `VdoConversionScanner` iterates
    over the contained lsblk information and checks each disk or partition for
    being a VDO device.  There are four categories of devices in the eyes of
    `VdoConversionScanner`:

      - not a VDO device
      - a pre-conversion VDO device
      - a post-conversion VDO device
      - a device not falling into any of the above

    Attempts to definitively identify a device as belonging to one of the first
    three listed categories above may fail.  These devices may or may not be an
    issue for upgrade.

    If a device could not be identified as either a VDO device or not results
    in that device's information being recorded in a
    VdoConversionUndeterminedDevice model.  This includes both the situation
    where LVM is installed on the system but the VDO management software is
    not as well as the situation where both are installed but the check of
    the device encountered an unexpected error.

    Devices identified as not a VDO device are skipped.

    Devices identified as pre-conversion VDOs have their identifying data
    stored in a VdoConversionPreDevice model; their simple existence is
    sufficient reason to prevent upgrade.

    A post-conversion (at VDO level) VDO device may not have completed its
    conversion to LVM-based management (e.g., via a poorly timed system crash
    during the conversion). For those VDO device's identified as
    post-conversion `VdoConversionScanner` performs an additional check to
    determine if the device is identified by blkid as an LVM2_member.  As the
    invocation of blkid may fail for reasons outside this scanner's control if
    such happens the device's completion status will be set to indicate it did
    not complete conversion.

    The generated VdoConversionPostDevice, VdoConversionPreDevice and
    VdoConversionUndeterminedDevice models are used together to produce the
    VdoConversionInfo model.  This latter model is consumed by the CheckVdo
    actor (executed during ChecksPhase) which, based on the contents of the
    model, may produce upgrade inhibitory reports.
    """

    name = 'vdo_conversion_scanner'
    consumes = (InstalledRedHatSignedRPM, StorageInfo)
    produces = (VdoConversionInfo,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        for storage_info in self.consume(StorageInfo):
            self.produce(vdoconversionscanner.get_info(storage_info))


# repos/common/topics/systeminfo.py

from leapp.topics import Topic


class SystemInfoTopic(Topic):
    name = 'system_info'


# commands/command_utils.py

import json
import os
import re

from leapp.exceptions import CommandError
from leapp.utils import path

HANA_BASE_PATH = '/hana/shared'
HANA_SAPCONTROL_PATH = 'exe/linuxx86_64/hdb/sapcontrol'

LEAPP_UPGRADE_FLAVOUR_DEFAULT = 'default'
LEAPP_UPGRADE_FLAVOUR_SAP_HANA = 'saphana'
LEAPP_UPGRADE_PATHS = 'upgrade_paths.json'

VERSION_REGEX = re.compile(r"^([1-9]\d*)\.(\d+)$")


def check_version(version):
    """
    Versioning schema: MAJOR.MINOR
    In case version contains an invalid version string, an CommandError will be raised.

    :raises: CommandError
    :return: release tuple
    """
    if not re.match(VERSION_REGEX, version):
        raise CommandError('Unexpected format of target version: {}'.format(version))
    return version.split('.')[0]


def get_major_version(version):
    """
    Return the major version from the given version string.

    Versioning schema: MAJOR.MINOR.PATCH

    :param str version: The version string according to the versioning schema described.
    :rtype: str
    :returns: The major version from the given version string.
    """
    return str(check_version(version)[0])


def detect_sap_hana():
    """
    Detect SAP HANA based on existance of /hana/shared/*/exe/linuxx86_64/hdb/sapcontrol
    """
    if os.path.exists(HANA_BASE_PATH):
        for entry in os.listdir(HANA_BASE_PATH):
            # Does /hana/shared/{entry}/exe/linuxx86_64/hdb/sapcontrol exist?
            if os.path.exists(os.path.join(HANA_BASE_PATH, entry, HANA_SAPCONTROL_PATH)):
                return True
    return False


def get_upgrade_flavour():
    """
    Returns the flavour of the upgrade for this system.
    """
    if detect_sap_hana():
        return LEAPP_UPGRADE_FLAVOUR_SAP_HANA
    return LEAPP_UPGRADE_FLAVOUR_DEFAULT


def get_os_release_version_id(filepath):
    """
    Retrieve data about System OS release from provided file.

    :return: `str` version_id
    """
    with open(filepath) as f:
        data = dict(l.strip().split('=', 1) for l in f.readlines() if '=' in l)
        return data.get('VERSION_ID', '').strip('"')


def get_upgrade_paths_config():
    # NOTE(ivasilev) Importing here not to have circular dependencies
    from leapp.cli.commands.upgrade import util  # noqa: C415; pylint: disable=import-outside-toplevel

    repository = util.load_repositories_from('repo_path', '/etc/leapp/repo.d/', manager=None)
    with open(path.get_common_file_path(repository, LEAPP_UPGRADE_PATHS)) as f:
        upgrade_paths_map = json.loads(f.read())
    return upgrade_paths_map


def get_target_versions_from_config(src_version_id, flavor):
    """
    Retrieve all possible target versions from upgrade_paths_map.
    If no match is found returns empty list.
    """
    upgrade_paths_map = get_upgrade_paths_config()
    return upgrade_paths_map.get(flavor, {}).get(src_version_id, [])


def get_supported_target_versions(flavour=get_upgrade_flavour()):
    """
    Return a list of supported target versions for the given `flavour` of upgrade.
    The default value for `flavour` is `default`.
    """

    current_version_id = get_os_release_version_id('/etc/os-release')
    target_versions = get_target_versions_from_config(current_version_id, flavour)
    if not target_versions:
        # If we cannot find a particular major.minor version in the map,
        # we fallback to pick a target version just based on a major version.
        # This can happen for example when testing not yet released versions
        major_version = get_major_version(current_version_id)
        target_versions = get_target_versions_from_config(major_version, flavour)

    return target_versions


def get_target_version(flavour):
    target_versions = get_supported_target_versions(flavour)
    return target_versions[-1] if target_versions else None


def vet_upgrade_path(args):
    """
    Make sure the user requested upgrade_path is a supported one.
    If LEAPP_DEVEL_TARGET_RELEASE is set then it's value is not vetted against upgrade_paths_map but used as is.

    :raises: `CommandError` if the specified upgrade_path is not supported
    :return: `tuple` (target_release, flavor)
    """
    flavor = get_upgrade_flavour()
    env_version_override = os.getenv('LEAPP_DEVEL_TARGET_RELEASE')
    if env_version_override:
        check_version(env_version_override)
        return (env_version_override, flavor)
    target_release = args.target or get_target_version(flavor)
    supported_target_versions = get_supported_target_versions(flavor)
    if target_release not in supported_target_versions:
        raise CommandError(
                "Upgrade to {to} for {flavor} upgrade path is not supported, possible choices are {choices}".format(
                    to=target_release,
                    flavor=flavor,
                    choices=','.join(supported_target_versions)))
    return (target_release, flavor)


# utils/actor_path.py

import logging
import sys

from leapp.repository.scan import find_and_scan_repositories

def err_exit():
    # We want to be sure that `make test` (test_no_lint) will stop when expected
    # actor is not found and want to be sure that users will not overlook error
    # messages. This print will easily resolve the problem
    sys.stdout.write('ERROR:__read_error_messages_above_this_one_on_stderr__')
    sys.exit(1)

def main():
    logging.basicConfig(level=logging.INFO, filename='/dev/null')
    logger = logging.getLogger('run_pytest.py')

    BASE_REPO = 'repos'
    repos = find_and_scan_repositories(BASE_REPO, include_locals=True)
    repos.load()
    if len(sys.argv) > 1:
        actors = repos._lookup_actors(sys.argv[1])
        if not actors:
            sys.stderr.write('ERROR: No actor found for search "{}"\n'.format(sys.argv[1]))
            err_exit()
        print(' '.join([actor.full_path for actor in actors]))
    else:
        sys.stderr.write('ERROR: Missing commandline argument\n')
        err_exit()


if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        # Make a generic exception as in case of error, without the explicit
        # sys.exit(1) call running of tests continues with all actors always
        # and we expect that many people will be affected unless they installed
        sys.stderr.write('ERROR: Unknown error: {}\n'.format(e))
        sys.stderr.write('ERROR: Possibly you need newer version of the leapp framework\n')
        sys.stderr.write('ERROR:   e.g.: rm -rf .tut && make install-deps-fedora\n')
        err_exit()


# utils/dashboard-json-dump.py

from __future__ import print_function
import json

from leapp.utils.repository import find_repository_basedir
from leapp.repository.scan import find_and_scan_repositories

base_dir = find_repository_basedir('.')
repository = find_and_scan_repositories(base_dir, include_locals=True)

repository.load()

if not hasattr(repository, 'repos'):
    repository.repos = [repository]

print(json.dumps([repo.serialize() for repo in repository.repos]))


# utils/install_actor_deps.py

"""
Script for installing dependencies for specific actor.
It is called from Makefile install-deps target.
If given actor doesn't exist, script exits with return
code 1 and stderr message.
If given actor doesn't have Makefile, warning printed on stderr.
If no actor is specified dependencies will be installed
for all actors with Makefile.

usage: python install_actor_deps.py [--actor actorname]
"""

import argparse
import os
import sys
from subprocess import check_call, CalledProcessError


def error(msg, rc):
    sys.stderr.write(msg)
    sys.exit(rc)


def install(path):
    cmd = "make -f {} install-deps".format(path)
    try:
        check_call(cmd, shell=True)
    except CalledProcessError as e:
        error(str(e) + '\n', e.returncode)


def install_actor_deps(actor, directory):
    for root, dirs, files in os.walk(directory):
        if actor in dirs:
            makefile_path = os.path.join(root, actor, 'Makefile')
            if os.path.isfile(makefile_path):
                install(makefile_path)
            else:
                sys.stderr.write("Actor '{}' doesn't have Makefile!\n".format(actor))
            return
    error("Actor '{}' doesn't exist!\n".format(actor), 1)


def install_all_deps(directory, repos):
    repos = repos.split() if repos else repos
    for root, dirs, files in os.walk(directory):
        if repos and not any([repo in dirs for repo in repos]):
            continue
        if 'Makefile' in files:
            install(os.path.join(root, 'Makefile'))


if __name__ == "__main__":
    ACTORS_DIR = './repos'

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--actor", help="name of the actor for which to install dependencies")
    parser.add_argument(
        "--repos", help="repositories to look into")
    args = parser.parse_args()

    if args.actor:
        install_actor_deps(args.actor, ACTORS_DIR)
    else:
        install_all_deps(ACTORS_DIR, args.repos)


# utils/library_path.py

import logging
import sys

from leapp.repository.scan import find_and_scan_repositories


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, filename='/dev/null')
    logger = logging.getLogger('run_pytest.py')

    BASE_REPO = 'repos'
    repos = find_and_scan_repositories(BASE_REPO, include_locals=True)
    repos.load()

    print(' '.join(repos.libraries))


# repos/system_upgrade/el7toel8/actors/authselectscanner/libraries/authselectscanner.py

import os
import re
import textwrap

from six import StringIO

from leapp.libraries.stdlib import run, CalledProcessError
from leapp.libraries.common import utils
from leapp.models import Authselect


def read_file(config):
    """
        Read file contents. Return empty string if the file does not exist.
    """
    if not os.path.isfile(config):
        return ""
    with open(config) as f:
        return f.read()


def is_service_enabled(service):
    """
        Return true if @service is enabled with systemd, false otherwise.
    """
    try:
        run(["/usr/bin/systemctl", "is-enabled", "{}.service".format(service)])
    except (OSError, CalledProcessError):
        return False

    return True


class ConfigFile(object):
    """
        Base class for config parsers.
    """

    def __init__(self, content):
        parser = utils.parse_config(StringIO(textwrap.dedent(content)))
        self.config = parser

    def get_string(self, section, option):
        if not self.config.has_option(section, option):
            return None

        return self.config.get(section, option).strip('"\'')

    def get_bool(self, section, option):
        if not self.config.has_option(section, option):
            return False

        return self.config.getboolean(section, option)


class Authconfig(ConfigFile):
    """
        Parse authconfig configuration.
    """

    def __init__(self, config):
        # We add a custom section to convert the config to ini format
        super(Authconfig, self).__init__('[authconfig]\n' + config)

    def get_string(self, option):
        return super(Authconfig, self).get_string('authconfig', option)

    def get_bool(self, option):
        return super(Authconfig, self).get_bool('authconfig', option)


class DConf(ConfigFile):
    """
        Parse dconf configuration.
    """


class AuthselectScannerLibrary(object):
    """
    Detect what authselect configuration should be suggested to administrator.

    1. Detect possible authselect profile by looking up modules in PAM
       or by checking that daemon is enabled.
       - pam_sss -> sssd
       - pam_winbind -> winbind
       - ypbind enabled -> nis

       If more then one module/daemon is detected that we will keep the
       configuration intact. No authselect profile can be applied.

    2. Detect authselect profile features by looking up modules in PAM
       or nsswitch.conf.
       - pam_faillock => with-faillock
       - pam_fprintd => with-fingerprint
       - pam_access => with-pamaccess
       - pam_mkhomedir => with-mkhomedir
       - pam_oddjob_mkhomedir => with-mkhomedir

    3. Check if there are any unknown PAM modules.
       If there are used PAM modules not used in authselect (such as pam_ldap),
       we must keep the configuration intact.

    4. Check if authconfig was used to create current configuration.
       If yes, we can automatically convert the configuration to authselect.
       If no, we need admin's confirmation.

       - Check that /etc/sysconfig/authconfig exists.
       - Check that PAM configuration uses authconfig files.
       - Check that PAM configuration was not touch after sysconfig file
         was created.
    """

    def __init__(self, known_modules, authconfig, dconf, pam, nsswitch):
        self.known_modules = known_modules
        self.ac = authconfig
        self.dconf = dconf
        self.pam = pam
        self.nsswitch = nsswitch

        self.profile = None
        self.features = []
        self.confirm = True

    def process(self):
        # Detect possible authselect configuration
        self.profile = self.step_detect_profile()
        self.features += self.step_detect_features()
        self.features += self.step_detect_sssd_features(self.profile)
        self.features += self.step_detect_winbind_features(self.profile)

        # Check if there is any module that is not known by authselect.
        # In this case we must left existing configuration intact.
        if self.pam.has_unknown_module(self.known_modules):
            self.profile = None
            self.features = []

        # Check if the proposed authselect configuration can be activated
        # automatically or admin's confirmation is required.
        self.confirm = self.step_detect_if_confirmation_is_required()

        # Remove duplicates
        self.features = sorted(set(self.features))

        return Authselect(
            profile=self.profile,
            features=self.features,
            confirm=self.confirm
        )

    def step_detect_profile(self):
        """
        Authselect supports three different profiles:
          - sssd
          - winbind
          - nis

        Only one of these profiles can be selected therefore if existing
        configuration contains combination of these daemons we can not
        suggest any profile and must keep existing configuration.
        """
        enabled_no = 0
        profile = None

        if self.pam.has('pam_sss'):
            profile = 'sssd'
            enabled_no += 1

        if self.pam.has('pam_winbind'):
            profile = 'winbind'
            enabled_no += 1

        if is_service_enabled('ypbind'):
            profile = 'nis'
            enabled_no += 1

        return profile if enabled_no == 1 else None

    def step_detect_features(self):
        pam_map = {
            'pam_faillock': 'with-faillock',
            'pam_fprintd': 'with-fingerprint',
            'pam_access': 'with-pamaccess',
            'pam_mkhomedir': 'with-mkhomedir',
            'pam_oddjob_mkhomedir': 'with-mkhomedir'
        }

        features = []

        for module, feature in pam_map.items():
            if self.pam.has(module):
                features.append(feature)

        return features

    def step_detect_sssd_features(self, profile):
        if profile != "sssd":
            return []

        # sudoers: sss
        result = re.search(
            "^[ \t]*sudoers[ \t]*:.*sss.*$",
            self.nsswitch,
            re.MULTILINE
        )

        features = []

        if result is not None:
            features.append("with-sudo")

        # SSSD Smartcard support
        # We enable smartcard support only if it was not handled by pam_pkcs11.
        # Otherwise pam_pkcs11 configuration must be converted manually.
        if not self.pam.has('pam_pkcs11'):
            if self.ac.get_bool('USESMARTCARD'):
                features.append("with-smartcard")

            if self.ac.get_bool('FORCESMARTCARD'):
                features.append("with-smartcard-required")

            if self.dconf.get_string(
                    'org/gnome/settings-daemon/peripherals/smartcard',
                    'removal-action'
            ) == 'lock-screen':
                features.append("with-smartcard-lock-on-removal")

        return features

    def step_detect_winbind_features(self, profile):
        if profile != "winbind":
            return []

        if self.ac.get_bool('WINBINDKRB5'):
            return ['with-krb5']

        return []

    def step_detect_if_confirmation_is_required(self):
        sysconfig = '/etc/sysconfig/authconfig'
        links = {
            '/etc/pam.d/fingerprint-auth': '/etc/pam.d/fingerprint-auth-ac',
            '/etc/pam.d/password-auth': '/etc/pam.d/password-auth-ac',
            '/etc/pam.d/postlogin': '/etc/pam.d/postlogin-ac',
            '/etc/pam.d/smartcard-auth': '/etc/pam.d/smartcard-auth-ac',
            '/etc/pam.d/system-auth': '/etc/pam.d/system-auth-ac'
        }

        # Check that authconfig was used to create the configuration
        if not os.path.isfile(sysconfig):
            return True

        # Check that all files are symbolic links to authconfig files
        for name, target in links.items():
            if not os.path.islink(name):
                return True

            if os.readlink(name) != target:
                return True

        # Check that all file were not modified after
        # /etc/sysconfig/authconfig was created.
        mtime = os.path.getmtime(sysconfig)
        for f in links.values():
            if os.path.getmtime(f) > mtime:
                return True

        return False


# repos/system_upgrade/el7toel8/actors/authselectscanner/tests/unit_test_authselectscanner.py

import textwrap

from mock import patch

from leapp.libraries.actor.authselectscanner import Authconfig, AuthselectScannerLibrary, DConf, read_file
from leapp.libraries.common.pam import PAM


def get_config(config):
    return textwrap.dedent(config).strip()


def test_read_file__non_existent():
    content = read_file('/this/does/not/exist')
    assert content == ''


def test_read_file__ok():
    content = read_file(__file__)
    assert content != ''
    assert 'test_read_file__ok' in content


def test_Authconfig_get_bool__non_existent():
    obj = Authconfig('')
    assert not obj.get_bool('non-existent-option')


def test_Authconfig_get_bool__true():
    obj = Authconfig(get_config('''
    test_a=True
    test_b=true
    test_c=Yes
    test_d=yes
    '''))

    assert obj.get_bool('test_a')
    assert obj.get_bool('test_b')
    assert obj.get_bool('test_c')
    assert obj.get_bool('test_d')


def test_Authconfig_get_bool__false():
    obj = Authconfig(get_config('''
    test_a=False
    test_b=false
    test_c=No
    test_d=no
    '''))

    assert not obj.get_bool('test_a')
    assert not obj.get_bool('test_b')
    assert not obj.get_bool('test_c')
    assert not obj.get_bool('test_d')


def test_Authconfig_get_string__non_existent():
    obj = Authconfig('')
    assert obj.get_string('non-existent-option') is None


def test_Authconfig_get_string__ok():
    obj = Authconfig(get_config('''
    test_a="str"
    test_b=str
    '''))

    assert obj.get_string('test_a') == 'str'
    assert obj.get_string('test_b') == 'str'


def test_DConf_get_bool__non_existent():
    obj = DConf('')
    assert not obj.get_bool('section', 'non-existent-option')


def test_DConf_get_bool__true():
    obj = DConf(get_config('''
    [section]
    test_a=True
    test_b=true
    test_c=Yes
    test_d=yes
    '''))

    assert obj.get_bool('section', 'test_a')
    assert obj.get_bool('section', 'test_b')
    assert obj.get_bool('section', 'test_c')
    assert obj.get_bool('section', 'test_d')


def test_DConf_get_bool__false():
    obj = DConf(get_config('''
    [section]
    test_a=False
    test_b=false
    test_c=No
    test_d=no
    '''))

    assert not obj.get_bool('section', 'test_a')
    assert not obj.get_bool('section', 'test_b')
    assert not obj.get_bool('section', 'test_c')
    assert not obj.get_bool('section', 'test_d')


def test_DConf_get_string__non_existent():
    obj = DConf('')
    assert obj.get_string('section', 'non-existent-option') is None


def test_DConf_get_string__ok():
    obj = DConf(get_config('''
    [section]
    test_a="str"
    test_b=str
    '''))

    assert obj.get_string('section', 'test_a') == 'str'
    assert obj.get_string('section', 'test_b') == 'str'


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__None(mock_service):
    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(''), '')
    mock_service.return_value = False
    assert obj.step_detect_profile() is None


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__sssd(mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    mock_service.return_value = False
    assert obj.step_detect_profile() == 'sssd'


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__winbind(mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_winbind.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    mock_service.return_value = False
    assert obj.step_detect_profile() == 'winbind'


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__nis(mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    mock_service.return_value = True
    assert obj.step_detect_profile() == 'nis'


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__sssd_winbind(mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_winbind.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    mock_service.return_value = False
    assert obj.step_detect_profile() is None


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__sssd_nis(mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    mock_service.return_value = True
    assert obj.step_detect_profile() is None


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__winbind_nis(mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_winbind.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    mock_service.return_value = True
    assert obj.step_detect_profile() is None


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
def test_AuthselectScannerLibrary_step_detect_profile__sssd_winbind_nis(mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_winbind.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    mock_service.return_value = True
    assert obj.step_detect_profile() is None


def test_AuthselectScannerLibrary_step_detect_features__faillock():
    pam = get_config('''
    auth required pam_faillock.so preauth silent deny=4 unlock_time=1200
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    assert obj.step_detect_features() == ['with-faillock']


def test_AuthselectScannerLibrary_step_detect_features__fingerprint():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth sufficient pam_fprintd.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    assert obj.step_detect_features() == ['with-fingerprint']


def test_AuthselectScannerLibrary_step_detect_features__access():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    account required pam_access.so
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    assert obj.step_detect_features() == ['with-pamaccess']


def test_AuthselectScannerLibrary_step_detect_features__mkhomedir():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    session optional pam_mkhomedir.so umask=0077
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    assert obj.step_detect_features() == ['with-mkhomedir']


def test_AuthselectScannerLibrary_step_detect_features__mkhomedir_oddjob():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    session optional pam_oddjob_mkhomedir.so umask=0077
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    assert obj.step_detect_features() == ['with-mkhomedir']


def test_AuthselectScannerLibrary_step_detect_features__all():
    pam = get_config('''
    auth required pam_faillock.so preauth silent deny=4 unlock_time=1200
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth sufficient pam_fprintd.so
    auth required pam_deny.so
    account required pam_access.so
    session optional pam_oddjob_mkhomedir.so umask=0077
    ''')

    obj = AuthselectScannerLibrary([], Authconfig(''), DConf(''), PAM(pam), '')
    features = obj.step_detect_features()
    assert len(features) == 4
    assert 'with-faillock' in features
    assert 'with-fingerprint' in features
    assert 'with-pamaccess' in features
    assert 'with-mkhomedir' in features


def test_AuthselectScannerLibrary_step_detect_sssd_features__sudo():
    nsswitch = get_config('''
    passwd:     files sss systemd
    group:      files sss systemd
    sudoers:    files sss
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(''), PAM(''), nsswitch
    )
    features = obj.step_detect_sssd_features('sssd')
    assert features == ['with-sudo']


def test_AuthselectScannerLibrary_step_detect_sssd_features__smartcard():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    ac = get_config('''
    USESMARTCARD=yes
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(ac), DConf(''), PAM(pam), ''
    )
    features = obj.step_detect_sssd_features('sssd')
    assert features == ['with-smartcard']


def test_AuthselectScannerLibrary_step_detect_sssd_features__smartcard_required():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    ac = get_config('''
    FORCESMARTCARD=yes
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(ac), DConf(''), PAM(pam), ''
    )
    features = obj.step_detect_sssd_features('sssd')
    assert features == ['with-smartcard-required']


def test_AuthselectScannerLibrary_step_detect_sssd_features__smartcard_lock():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    dconf = get_config('''
    [org/gnome/settings-daemon/peripherals/smartcard]
    removal-action='lock-screen'
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(dconf), PAM(pam), ''
    )
    features = obj.step_detect_sssd_features('sssd')
    assert features == ['with-smartcard-lock-on-removal']


def test_AuthselectScannerLibrary_step_detect_sssd_features__pkcs11():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_pkcs11.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    ac = get_config('''
    USESMARTCARD=yes
    FORCESMARTCARD=yes
    ''')

    dconf = get_config('''
    [org/gnome/settings-daemon/peripherals/smartcard]
    removal-action='lock-screen'
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(ac), DConf(dconf), PAM(pam), ''
    )
    features = obj.step_detect_sssd_features('sssd')
    assert not features


def test_AuthselectScannerLibrary_step_detect_sssd_features__wrong_profile():
    nsswitch = get_config('''
    passwd:     files sss systemd
    group:      files sss systemd
    sudoers:    files sss
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(''), PAM(''), nsswitch
    )
    features = obj.step_detect_sssd_features('winbind')
    assert not features


def test_AuthselectScannerLibrary_step_detect_winbind_features__krb5():
    ac = get_config('''
    WINBINDKRB5=yes
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(ac), DConf(''), PAM(''), ''
    )
    features = obj.step_detect_winbind_features('winbind')
    assert features == ['with-krb5']


def test_AuthselectScannerLibrary_step_detect_winbind_features__wrong_profile():
    ac = get_config('''
    WINBINDKRB5=yes
    ''')

    obj = AuthselectScannerLibrary(
        [], Authconfig(ac), DConf(''), PAM(''), ''
    )
    features = obj.step_detect_winbind_features('sssd')
    assert not features


@patch('os.readlink')
@patch('os.path.islink')
@patch('os.path.isfile')
@patch('os.path.getmtime')
def test_AuthselectScannerLibrary_step_detect_if_confirmation_is_required__nosysconfig(
    mock_getmtime, mock_isfile, mock_islink, mock_readlink
):
    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(''), PAM(''), ''
    )
    mock_isfile.return_value = False
    assert obj.step_detect_if_confirmation_is_required()


@patch('os.readlink')
@patch('os.path.islink')
@patch('os.path.isfile')
@patch('os.path.getmtime')
def test_AuthselectScannerLibrary_step_detect_if_confirmation_is_required__nolink(
    mock_getmtime, mock_isfile, mock_islink, mock_readlink
):
    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(''), PAM(''), ''
    )
    mock_isfile.return_value = True
    mock_islink.return_value = False
    assert obj.step_detect_if_confirmation_is_required()


@patch('os.readlink')
@patch('os.path.islink')
@patch('os.path.isfile')
@patch('os.path.getmtime')
def test_AuthselectScannerLibrary_step_detect_if_confirmation_is_required__badlink(
    mock_getmtime, mock_isfile, mock_islink, mock_readlink
):
    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(''), PAM(''), ''
    )
    mock_isfile.return_value = True
    mock_islink.return_value = True
    mock_readlink.return_value = ''
    assert obj.step_detect_if_confirmation_is_required()


@patch('os.readlink')
@patch('os.path.islink')
@patch('os.path.isfile')
@patch('os.path.getmtime')
def test_AuthselectScannerLibrary_step_detect_if_confirmation_is_required__badmtime(
    mock_getmtime, mock_isfile, mock_islink, mock_readlink
):
    def my_getmtime(path):
        # Make sysconfig file older then other files.
        if path == '/etc/sysconfig/authconfig':
            return 100

        return 200

    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(''), PAM(''), ''
    )
    mock_isfile.return_value = True
    mock_islink.return_value = True
    mock_readlink.side_effect = '{}-ac'.format
    mock_getmtime.side_effect = my_getmtime
    assert obj.step_detect_if_confirmation_is_required()


@patch('os.readlink')
@patch('os.path.islink')
@patch('os.path.isfile')
@patch('os.path.getmtime')
def test_AuthselectScannerLibrary_step_detect_if_confirmation_is_required__pass(
    mock_getmtime, mock_isfile, mock_islink, mock_readlink
):
    def my_getmtime(path):
        # Make sysconfig file younger then other files.
        if path == '/etc/sysconfig/authconfig':
            return 200

        return 100

    obj = AuthselectScannerLibrary(
        [], Authconfig(''), DConf(''), PAM(''), ''
    )
    mock_isfile.return_value = True
    mock_islink.return_value = True
    mock_readlink.side_effect = '{}-ac'.format
    mock_getmtime.side_effect = my_getmtime
    assert not obj.step_detect_if_confirmation_is_required()


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
@patch('leapp.libraries.actor.authselectscanner.AuthselectScannerLibrary.step_detect_if_confirmation_is_required')
def test_AuthselectScannerLibrary_process__simple(mock_confirm, mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary(
        ['pam_unix', 'pam_sss', 'pam_deny'], Authconfig(''), DConf(''), PAM(pam), ''
    )
    mock_confirm.return_value = True
    mock_service.return_value = False
    authselect = obj.process()
    assert authselect.profile == 'sssd'
    assert not authselect.features
    assert authselect.confirm


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
@patch('leapp.libraries.actor.authselectscanner.AuthselectScannerLibrary.step_detect_if_confirmation_is_required')
def test_AuthselectScannerLibrary_process__features(mock_confirm, mock_service):
    pam = get_config('''
    auth required pam_faillock.so preauth silent deny=4 unlock_time=1200
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    nsswitch = get_config('''
    passwd:     files sss systemd
    group:      files sss systemd
    sudoers:    files sss
    ''')

    obj = AuthselectScannerLibrary(
        ['pam_unix', 'pam_sss', 'pam_deny', 'pam_faillock'],
        Authconfig(''),
        DConf(''),
        PAM(pam),
        nsswitch
    )
    mock_confirm.return_value = True
    mock_service.return_value = False
    authselect = obj.process()
    assert authselect.profile == 'sssd'
    assert len(authselect.features) == 2
    assert 'with-faillock' in authselect.features
    assert 'with-sudo' in authselect.features
    assert authselect.confirm


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
@patch('leapp.libraries.actor.authselectscanner.AuthselectScannerLibrary.step_detect_if_confirmation_is_required')
def test_AuthselectScannerLibrary_process__unknown_module(mock_confirm, mock_service):
    pam = get_config('''
    auth required pam_faillock.so preauth silent deny=4 unlock_time=1200
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary(
        ['pam_unix', 'pam_sss', 'pam_deny'],
        Authconfig(''),
        DConf(''),
        PAM(pam),
        ''
    )
    mock_confirm.return_value = True
    mock_service.return_value = False
    authselect = obj.process()
    assert authselect.profile is None
    assert not authselect.features
    assert authselect.confirm


@patch('leapp.libraries.actor.authselectscanner.is_service_enabled')
@patch('leapp.libraries.actor.authselectscanner.AuthselectScannerLibrary.step_detect_if_confirmation_is_required')
def test_AuthselectScannerLibrary_process__autoconfirm(mock_confirm, mock_service):
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = AuthselectScannerLibrary(
        ['pam_unix', 'pam_sss', 'pam_deny'], Authconfig(''), DConf(''), PAM(pam), ''
    )
    mock_confirm.return_value = False
    mock_service.return_value = False
    authselect = obj.process()
    assert authselect.profile == 'sssd'
    assert not authselect.features
    assert not authselect.confirm


# repos/system_upgrade/el7toel8/actors/checkacpid/tests/component_test_checkacpid.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import InstalledRedHatSignedRPM, RPM
from leapp.reporting import Report

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_with_acpid_package(current_actor_context):
    with_acpid = [
        RPM(name='acpid', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=with_acpid))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


def test_actor_without_acpid_package(current_actor_context):
    without_acpid = [
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=without_acpid))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkbrltty/libraries/checkbrltty.py

import os
import re

BrlttyConf = '/etc/brltty.conf'


def check_for_unsupported_cfg():
    migrate_file = None
    migrate_bt = False
    migrate_espeak = False
    regex_bt = re.compile(r'\b((bth)|(bluez))([:\-][0-9a-fA-F]{2}){6}\b')
    regex_espeak = re.compile(r'^\s*speech-driver\s+es\b')
    if os.path.exists(BrlttyConf):
        with open(BrlttyConf) as file_check:
            for line in file_check:
                if regex_bt.search(line):
                    migrate_bt = True
                if regex_espeak.search(line):
                    migrate_espeak = True
                if migrate_bt and migrate_espeak:
                    break
    migrate_file = BrlttyConf if migrate_espeak or migrate_bt else ''
    return (migrate_file, migrate_bt, migrate_espeak)


# repos/system_upgrade/el7toel8/actors/checkbrltty/tests/component_test_checkbrltty.py

from leapp.models import InstalledRedHatSignedRPM, RPM, BrlttyMigrationDecision
from leapp.reporting import Report

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'

with_brltty = [
    RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
    RPM(name='brltty', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

without_brltty = [
    RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
    RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_without_brltty_package(current_actor_context):
    current_actor_context.feed(create_modulesfacts(installed_rpm=without_brltty))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_actor_with_brltty_package(current_actor_context):
    current_actor_context.feed(create_modulesfacts(installed_rpm=with_brltty))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkbrltty/tests/unit_test_checkbrltty.py

import pytest
from six import text_type

from leapp.libraries.actor import checkbrltty

BRLTTY_CONF = 'brltty.conf'


@pytest.mark.parametrize('test_input,expected_migrate_bt,expected_migrate_espeak', [
    ('braille-device serial:/dev/ttyS0\n', False, False),
    ('braille-device bth:AB-cd:ef:01:23:45\n', True, False),
    ('braille-device bluez:AB-cd:ef:01:23:45\n', True, False),
    ('speech-driver es\n', False, True),
    ('braille-device bth:AB-cd:ef:01:23:45\nbraille-device bluez:AB-cd:ef:01:23:45\nspeech-driver es\n', True, True),
])
def test_actor_check_migration_bth(tmpdir, monkeypatch, test_input, expected_migrate_bt, expected_migrate_espeak,
                                   current_actor_context):
    test_cfg_file = text_type(tmpdir.join(BRLTTY_CONF))
    with open(test_cfg_file, 'w') as file_out:
        file_out.write(test_input)
    monkeypatch.setattr(checkbrltty, 'BrlttyConf', test_cfg_file)
    (migrate_file, migrate_bt, migrate_espeak,) = checkbrltty.check_for_unsupported_cfg()

    if expected_migrate_bt or expected_migrate_espeak:
        assert test_cfg_file == migrate_file
    assert expected_migrate_bt == migrate_bt
    assert expected_migrate_espeak == migrate_espeak


# repos/system_upgrade/el7toel8/actors/checkbtrfs/tests/test_btrfs_checkbtrfs.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import ActiveKernelModule, ActiveKernelModulesFacts
from leapp.reporting import Report


def create_modulesfacts(kernel_modules):
    return ActiveKernelModulesFacts(kernel_modules=kernel_modules)


def test_actor_with_btrfs_module(current_actor_context):
    with_btrfs = [
        ActiveKernelModule(filename='btrfs', parameters=[]),
        ActiveKernelModule(filename='kvm', parameters=[])]

    current_actor_context.feed(create_modulesfacts(kernel_modules=with_btrfs))
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report
    assert 'inhibitor' in report_fields['flags']


def test_actor_without_btrfs_module(current_actor_context):
    without_btrfs = [
        ActiveKernelModule(filename='kvm_intel', parameters=[]),
        ActiveKernelModule(filename='kvm', parameters=[])]

    current_actor_context.feed(create_modulesfacts(kernel_modules=without_btrfs))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkchrony/libraries/checkchrony.py

from leapp import reporting
from leapp.libraries.stdlib import api, run


related = [
    reporting.RelatedResource('package', 'ntpd'),
    reporting.RelatedResource('package', 'chrony'),
    reporting.RelatedResource('file', '/etc/chrony.conf'),
]


def is_config_default():
    """Check if the chrony config file was not modified since installation."""
    try:
        result = run(['rpm', '-V', '--nomtime', 'chrony'], checked=False)
        return '/etc/chrony.conf' not in result['stdout']
    except OSError as e:
        api.current_logger().warning("rpm verification failed: %s", str(e))
        return True


def check_chrony(chrony_installed):
    """Report potential issues in chrony configuration."""
    if not chrony_installed:
        api.current_logger().info('chrony package is not installed')
        return

    if is_config_default():
        reporting.create_report([
            reporting.Title('chrony using default configuration'),
            reporting.Summary(
                'default chrony configuration in RHEL8 uses leapsectz directive, which cannot be used with '
                'leap smearing NTP servers, and uses a single pool directive instead of four server directives'
            ),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Tags([
                    reporting.Tags.SERVICES,
                    reporting.Tags.TIME_MANAGEMENT
            ])
        ] + related)

    else:
        reporting.create_report([
            reporting.Title('chrony using non-default configuration'),
            reporting.Summary('chrony behavior will not change in RHEL8'),
            reporting.Severity(reporting.Severity.LOW),
            reporting.Tags([
                    reporting.Tags.SERVICES,
                    reporting.Tags.TIME_MANAGEMENT
            ])
        ] + related)


# repos/system_upgrade/el7toel8/actors/checkchrony/tests/unit_test_checkchrony.py

from leapp.libraries.actor import checkchrony
from leapp.libraries.common.testutils import create_report_mocked
from leapp import reporting


def test_uninstalled(monkeypatch):
    for config_default in (False, True):
        monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
        monkeypatch.setattr(checkchrony, 'is_config_default', lambda: config_default)

        checkchrony.check_chrony(False)

        assert reporting.create_report.called == 0


def test_installed_defconf(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(checkchrony, 'is_config_default', lambda: True)

    checkchrony.check_chrony(True)

    assert reporting.create_report.called == 1
    assert reporting.create_report.report_fields['title'] == 'chrony using default configuration'


def test_installed_nodefconf(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(checkchrony, 'is_config_default', lambda: False)

    checkchrony.check_chrony(True)

    assert reporting.create_report.called == 1
    assert reporting.create_report.report_fields['title'] == 'chrony using non-default configuration'


# repos/system_upgrade/el7toel8/actors/checkcifs/libraries/checkcifs.py

from leapp import reporting
from leapp.reporting import create_report


def checkcifs(storage_info):
    for storage in storage_info:
        if any(entry.fs_vfstype == "cifs" for entry in storage.fstab):
            create_report([
                reporting.Title("Use of CIFS detected. Upgrade can't proceed"),
                reporting.Summary("CIFS is currently not supported by the inplace upgrade."),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags([
                        reporting.Tags.FILESYSTEM,
                        reporting.Tags.NETWORK
                ]),
                reporting.Remediation(hint='Comment out CIFS entries to proceed with the upgrade.'),
                reporting.Flags([reporting.Flags.INHIBITOR]),
                reporting.RelatedResource('file', '/etc/fstab')
            ])


# repos/system_upgrade/el7toel8/actors/checkcifs/tests/test_checkcifs.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import StorageInfo, FstabEntry
from leapp.reporting import Report


def test_actor_with_fstab_entry(current_actor_context):
    with_fstab_entry = [FstabEntry(fs_spec="//10.20.30.42/share1", fs_file="/mnt/win_share1",
                                   fs_vfstype="cifs",
                                   fs_mntops="credentials=/etc/win-credentials,file_mode=0755,dir_mode=0755",
                                   fs_freq="0", fs_passno="0"),
                        FstabEntry(fs_spec="//10.20.30.42/share2", fs_file="/mnt/win_share2",
                                   fs_vfstype="cifs",
                                   fs_mntops="credentials=/etc/win-credentials,file_mode=0755,dir_mode=0755",
                                   fs_freq="0", fs_passno="0"),
                        FstabEntry(fs_spec="/dev/mapper/fedora-home", fs_file="/home",
                                   fs_vfstype="ext4",
                                   fs_mntops="defaults,x-systemd.device-timeout=0",
                                   fs_freq="1", fs_passno="2")]
    current_actor_context.feed(StorageInfo(fstab=with_fstab_entry))
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report
    assert 'inhibitor' in report_fields['flags']
    assert report_fields['severity'] == 'high'
    assert report_fields['title'] == "Use of CIFS detected. Upgrade can't proceed"


def test_actor_no_cifs(current_actor_context):
    with_fstab_entry = [FstabEntry(fs_spec="/dev/mapper/fedora-home", fs_file="/home",
                                   fs_vfstype="ext4",
                                   fs_mntops="defaults,x-systemd.device-timeout=0",
                                   fs_freq="1", fs_passno="2")]
    current_actor_context.feed(StorageInfo(fstab=with_fstab_entry))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkcpu/libraries/cpu.py


from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import CPUInfo

SUPPORTED_MACHINE_TYPES = [2964, 2965, 3906, 3907, 8561, 8562]


def process():
    if not architecture.matches_architecture(architecture.ARCH_S390X):
        return
    cpuinfo = next(api.consume(CPUInfo), None)
    if cpuinfo is None:
        raise StopActorExecutionError(message=("Missing information about CPU."))

    if not cpuinfo.machine_type:
        # this is not expected to happen, but in case...
        api.current_logger().warning("The machine (CPU) type is empty.")

    if cpuinfo.machine_type not in SUPPORTED_MACHINE_TYPES:
        summary = ("The system is not possible to upgrade because of unsupported"
                   " type of the processor. Based on the official documentation,"
                   " z13, z14 and z15 processors are supported on the Red Hat Enterprise"
                   " Linux 8 system for the IBM Z architecture. The supported processors"
                   " have machine types {}. The detected machine type of the CPU is '{}'."
                   .format(", ".join([str(i) for i in SUPPORTED_MACHINE_TYPES]), cpuinfo.machine_type))
        report = [
            reporting.Title("The processor is not supported by the target system."),
            reporting.Summary(summary),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SANITY]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.ExternalLink(
                title="Considerations in adopting RHEL 8",
                url=("https://access.redhat.com/ecosystem/hardware/#/search?p=1&"
                     "c_version=Red%20Hat%20Enterprise%20Linux%208&ch_architecture=s390x"))
        ]
        reporting.create_report(report)


# repos/system_upgrade/el7toel8/actors/checkcpu/tests/test_check_cpu_checkcpu.py

import logging

import pytest

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import cpu
from leapp.libraries.common import testutils
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import CPUInfo


def test_non_ibmz_arch(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_X86_64))
    monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
    cpu.process()
    assert not reporting.create_report.called


def test_ibmz_arch_missing_cpuinfo(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X))
    monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
    monkeypatch.setattr(api, 'consume', lambda x: iter([]))
    with pytest.raises(StopActorExecutionError):
        cpu.process()
    assert not reporting.create_report.called


def test_ibmz_cpu_supported(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X))
    monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
    for sup_arch in cpu.SUPPORTED_MACHINE_TYPES:
        monkeypatch.setattr(api, 'consume', lambda x: iter([CPUInfo(machine_type=sup_arch)]))
        cpu.process()
        assert not reporting.create_report.called


def test_ibmz_cpu_unsupported(monkeypatch):
    title_msg = 'The processor is not supported by the target system.'
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X))
    monkeypatch.setattr(api, 'consume', lambda x: iter([CPUInfo(machine_type=666)]))
    monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
    cpu.process()
    assert reporting.create_report.called
    assert title_msg == reporting.create_report.report_fields['title']
    assert reporting.Flags.INHIBITOR in reporting.create_report.report_fields['flags']


def test_ibmz_cpu_is_empty(monkeypatch, caplog):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X))
    monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
    monkeypatch.setattr(api, 'consume', lambda x: iter([CPUInfo(machine_type=None)]))
    with caplog.at_level(logging.DEBUG):
        cpu.process()
    assert 'The machine (CPU) type is empty.' in caplog.text


# repos/system_upgrade/el7toel8/actors/checkdosfstools/tests/component_test_checkdosfstools.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import InstalledRedHatSignedRPM, RPM
from leapp.reporting import Report

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_with_dosfstools_package(current_actor_context):
    with_dosfstools = [
        RPM(name='dosfstools', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=with_dosfstools))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


def test_actor_without_dosfstools_package(current_actor_context):
    without_dosfstools = [
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=without_dosfstools))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkfirewalld/libraries/private.py

def isIpsetTypeSupportedByNftables(ipset_type):
    if ipset_type in ['hash:ip', 'hash:mac', 'hash:net']:
        return True

    return False


def isEbtablesTableSupported(table):
    if table in ['filter', 'nat']:
        return True

    return False


# repos/system_upgrade/el7toel8/actors/checkfirewalld/tests/component_test_checkfirewalld.py

from leapp.models import FirewalldFacts
from leapp.reporting import Report


def test_actor_execution(current_actor_context):
    current_actor_context.feed(
        FirewalldFacts(firewall_config_command='',
                       ebtablesTablesInUse=['broute'],
                       ipsetTypesInUse=['hash:net,port']))
    current_actor_context.run()
    report_fileds = current_actor_context.consume(Report)[0].report
    assert 'inhibitor' in report_fileds['flags']


# repos/system_upgrade/el7toel8/actors/checkfirewalld/tests/unit_test_checkfirewalld.py

from leapp.libraries.actor import private


def test_checkfirewalld_ipset():
    assert private.isIpsetTypeSupportedByNftables('hash:mac')
    assert private.isIpsetTypeSupportedByNftables('hash:ip')
    assert private.isIpsetTypeSupportedByNftables('hash:net')

    assert not private.isIpsetTypeSupportedByNftables('hash:ip,mark')
    assert not private.isIpsetTypeSupportedByNftables('hash:ip,port')
    assert not private.isIpsetTypeSupportedByNftables('hash:ip,port,ip')
    assert not private.isIpsetTypeSupportedByNftables('hash:ip,port,net')
    assert not private.isIpsetTypeSupportedByNftables('hash:net,iface')
    assert not private.isIpsetTypeSupportedByNftables('hash:net,net')
    assert not private.isIpsetTypeSupportedByNftables('hash:net,port')


def test_checkfirewalld_ebtables():
    assert private.isEbtablesTableSupported('nat')
    assert private.isEbtablesTableSupported('filter')

    assert not private.isEbtablesTableSupported('broute')


# repos/system_upgrade/el7toel8/actors/checkfstabxfsoptions/libraries/checkfstabxfsoptions.py

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp.models import StorageInfo

# man 5 xfs
REMOVED_XFS_OPTIONS = set([
    # removed from kernel in 4.0
    'nodelaylog',
    'delaylog',
    'ihashsize',
    'irixsgid',
    'osyncisdsync',
    'osyncisosync',
    # removed from kernel in 4.19
    'nobarrier',
    'barrier',
])


def _get_storage_data():
    storage = next(api.consume(StorageInfo), None)
    if not storage:
        raise StopActorExecutionError('The StorageInfo message is not available.')
    if not storage.fstab:
        raise StopActorExecutionError('Data from the /etc/fstab file is missing.')
    return storage


def process():
    storage = _get_storage_data()
    used_removed_options = set()
    for entry in storage.fstab:
        if entry.fs_vfstype == 'xfs':
            # NOTE: some opts could have a value, like ihashsize=4096 - we want
            # just the name of the option (that's why the double-split)
            options = set([opt.split('=')[0] for opt in entry.fs_mntops.split(',')])
            used_removed_options.update(options.intersection(REMOVED_XFS_OPTIONS))

    if not used_removed_options:
        return

    list_separator_fmt = '\n    - '
    reporting.create_report([
        reporting.Title('Deprecated XFS mount options present in FSTAB.'),
        reporting.Summary(
            'Some XFS mount options are not supported on RHEL 8 and prevent'
            ' system from booting correctly if any of the reported XFS options are used.'
            ' filesystem:{}{}.'.format(
                list_separator_fmt,
                list_separator_fmt.join(list(REMOVED_XFS_OPTIONS)))),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Flags([reporting.Flags.INHIBITOR]),
        reporting.Tags([reporting.Tags.FILESYSTEM]),
        reporting.RelatedResource('file', '/etc/fstab'),
        reporting.Remediation(hint=(
            'Drop the following mount options from the /etc/fstab file for any'
            ' XFS filesystem: {}.'.format(', '.join(used_removed_options)))),
    ])


# repos/system_upgrade/el7toel8/actors/checkfstabxfsoptions/tests/test_checkfstabxfsoptions.py

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import checkfstabxfsoptions
from leapp.models import FstabEntry, StorageInfo
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context


def _myint_gen():
    i = 0
    while True:
        yield i
        i += 1


def _gen_fs_ent(fstype='ext4', mntops='auto', val=_myint_gen()):
    return FstabEntry(
        fs_spec='/path/spec/{}'.format(next(val)),
        fs_file='/path/file/{}'.format(next(val)),
        fs_vfstype=fstype,
        fs_mntops=mntops,
        fs_freq='1',
        fs_passno='1',
    )


@pytest.mark.parametrize('fstab', [
    [_gen_fs_ent()],
    [_gen_fs_ent() for dummy in range(4)],
    [_gen_fs_ent(), _gen_fs_ent('ext4', 'auto,quota,huge_file')],
    # checking that problematic options are ignored for non-xfs FS
    [_gen_fs_ent(), _gen_fs_ent('ext4', 'auto,barier,huge_file')],
    [_gen_fs_ent('ext4', i) for i in checkfstabxfsoptions.REMOVED_XFS_OPTIONS],
    [_gen_fs_ent(i, 'nobarrier') for i in ('ext4', 'ext3', 'vfat', 'btrfs')],
])
def test_no_xfs_option(fstab, current_actor_context):
    current_actor_context.feed(StorageInfo(fstab=fstab))
    current_actor_context.run()
    report = current_actor_context.consume(Report)
    assert not report


# each item == one fstab
problematic_fstabs = [[_gen_fs_ent('xfs', ','.join(checkfstabxfsoptions.REMOVED_XFS_OPTIONS))]]
for opt in checkfstabxfsoptions.REMOVED_XFS_OPTIONS:
    problematic_fstabs.append([_gen_fs_ent('xfs', opt)])
    problematic_fstabs.append([_gen_fs_ent(), _gen_fs_ent('xfs', opt)])
    problematic_fstabs.append([_gen_fs_ent(), _gen_fs_ent('xfs', opt), _gen_fs_ent()])
    pre_opts = '{},auto,quota'.format(opt)
    in_opts = 'auto,{},quota'.format(opt)
    post_opts = 'auto,quota,{}'.format(opt)
    problematic_fstabs.append([_gen_fs_ent(), _gen_fs_ent('xfs', pre_opts)])
    problematic_fstabs.append([_gen_fs_ent(), _gen_fs_ent('xfs', in_opts)])
    problematic_fstabs.append([_gen_fs_ent(), _gen_fs_ent('xfs', post_opts)])
# ensure we catch even cases when a value is expected to be specified; we know just this
# one case, so it should be representative it's working like that..
problematic_fstabs.append([_gen_fs_ent(), _gen_fs_ent('xfs', 'defaults,ihashsize=4096')])
problematic_fstabs.append([_gen_fs_ent(), _gen_fs_ent('xfs', 'defaults,ihashsize=4096,auto')])


@pytest.mark.parametrize('fstab', problematic_fstabs)
def test_removed_xfs_option(fstab, current_actor_context):
    current_actor_context.feed(StorageInfo(fstab=fstab))
    current_actor_context.run()
    report = current_actor_context.consume(Report)
    assert report and len(report) == 1
    assert 'inhibitor' in report[0].report['flags']


# repos/system_upgrade/el7toel8/actors/checkgrep/tests/component_test_checkgrep.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import InstalledRedHatSignedRPM, RPM
from leapp.reporting import Report

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_with_grep_package(current_actor_context):
    with_grep = [
        RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=with_grep))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


def test_actor_without_grep_package(current_actor_context):
    without_grep = [
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=without_grep))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkhacluster/libraries/checkhacluster.py

import os.path

from leapp import reporting
from leapp.reporting import create_report

COROSYNC_CONF_LOCATION = "/etc/corosync/corosync.conf"
CIB_LOCATION = "/var/lib/pacemaker/cib/cib.xml"


def inhibit(node_type):
    create_report([
        reporting.Title("Use of HA cluster detected. Upgrade can't proceed."),
        reporting.Summary(
            "HA cluster is not supported by the inplace upgrade.\n"
            "HA cluster configuration file(s) found."
            " It seems to be a cluster {0}.".format(node_type)
        ),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Tags([reporting.Tags.HIGH_AVAILABILITY]),
        reporting.Flags([reporting.Flags.INHIBITOR]),
        reporting.ExternalLink(
            url="https://access.redhat.com/articles/2059253",
            title=(
                "Recommended Practices for Applying Software Updates"
                " to a RHEL High Availability or Resilient Storage Cluster"
            ),
        ),
        reporting.Remediation(
            hint=(
                "Destroy the existing HA cluster"
                " or (if you have already removed HA cluster packages) remove"
                " configuration files {0} and {1}".format(
                    CIB_LOCATION,
                    COROSYNC_CONF_LOCATION,
                )
            ),
            commands=[["sh", "-c", "pcs cluster stop --all --wait && pcs cluster destroy --all"]]
        ),
        reporting.RelatedResource('file', COROSYNC_CONF_LOCATION),
        reporting.RelatedResource('file', CIB_LOCATION)
    ])


def check_ha_cluster():
    if os.path.isfile(COROSYNC_CONF_LOCATION):
        inhibit(node_type="node")
    elif os.path.isfile(CIB_LOCATION):
        inhibit(node_type="remote node")


# repos/system_upgrade/el7toel8/actors/checkhacluster/tests/test_check_ha_cluster_checkhacluster.py

from leapp.reporting import Report
from leapp.libraries.actor.checkhacluster import COROSYNC_CONF_LOCATION, CIB_LOCATION


def assert_inhibits(reports, node_type):
    assert len(reports) == 1
    report_fields = reports[0].report
    assert "inhibitor" in report_fields['flags']
    assert "cluster {0}".format(node_type) in report_fields["summary"]


def test_no_inhibit_when_no_ha_cluster(monkeypatch, current_actor_context):
    monkeypatch.setattr("os.path.isfile", lambda path: False)
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_inhibits_when_cluster_node(monkeypatch, current_actor_context):
    # NOTE(ivasilev) Limiting here the paths to mock not to cause unexpected side-effects
    # (original test had path: True)
    monkeypatch.setattr("os.path.isfile", lambda path: path in (COROSYNC_CONF_LOCATION, CIB_LOCATION))
    current_actor_context.run()
    assert_inhibits(current_actor_context.consume(Report), "node")


def test_inhibits_when_cluster_node_no_cib(monkeypatch, current_actor_context):
    monkeypatch.setattr(
        "os.path.isfile",
        lambda path: path == COROSYNC_CONF_LOCATION
    )
    current_actor_context.run()
    assert_inhibits(current_actor_context.consume(Report), "node")


def test_inhibits_when_cluster_remote_node(monkeypatch, current_actor_context):
    # NOTE(ivasilev) Limiting here the paths to mock not to cause unexpected side-effects
    # (original test had path: path != COROSYNC_CONF_LOCATION)
    monkeypatch.setattr(
        "os.path.isfile",
        lambda path: path == CIB_LOCATION
    )
    current_actor_context.run()
    assert_inhibits(current_actor_context.consume(Report), "remote node")


# repos/system_upgrade/el7toel8/actors/checkirssi/tests/component_test_checkirssi.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import InstalledRedHatSignedRPM, RPM
from leapp.reporting import Report

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_with_irssi_package(current_actor_context):
    with_irssi = [
        RPM(name='irssi', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=with_irssi))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


def test_actor_without_irssi_package(current_actor_context):
    without_irssi = [
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=without_irssi))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkkdeapps/libraries/checkkdeapps.py

from leapp.libraries.stdlib import api
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRPM


def get_kde_apps_info():
    installed = list()
    base_kde_apps = ("kde-baseapps",
                     "okular",
                     "ark",
                     "kdepim",
                     "konsole",
                     "gwenview",
                     "kdenetwork",
                     "kate",
                     "kwrite")

    api.current_logger().info("  Detecting installed KDE apps  ")
    api.current_logger().info("================================")
    for app in [application for application in base_kde_apps if has_package(InstalledRPM, application)]:
        api.current_logger().info("Application {0} is installed.".format(app))
        installed.append(app)
    api.current_logger().info("----------------------------------")

    return installed


# repos/system_upgrade/el7toel8/actors/checkkdeapps/tests/unit_test_checkkdeapps.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import InstalledRPM, RPM, InstalledKdeAppsFacts


RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'

#  KDE apps (only name matters, other values are irrelevant)
okular_RPM = RPM(name='okular', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
                 pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')
kdenetwork_RPM = RPM(name='kdenetwork', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER,
                     arch='noarch', pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')
kate_RPM = RPM(name='kate', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
               pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')

# Some other apps to check false detection (only name matters, other values are irrelevant)
epiphany_PRM = RPM(name='epiphany', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
                   pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')
polari_RPM = RPM(name='polari', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
                 pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')


def test_no_app_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledKdeAppsFacts)[0]
    assert not message.installed_apps


def test_no_KDE_app_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[epiphany_PRM, polari_RPM]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledKdeAppsFacts)[0]
    assert not message.installed_apps


def test_only_KDE_apps_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[okular_RPM, kdenetwork_RPM, kate_RPM]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledKdeAppsFacts)[0]
    assert len(message.installed_apps) == 3


def test_many_apps_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[okular_RPM, kdenetwork_RPM, kate_RPM, epiphany_PRM, polari_RPM]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledKdeAppsFacts)[0]
    assert len(message.installed_apps) == 3


# repos/system_upgrade/el7toel8/actors/checkkdegnome/libraries/checkkdegnome.py

from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.models import InstalledDesktopsFacts, InstalledKdeAppsFacts


def check_kde_gnome():
    desktop_facts = next(api.consume(InstalledDesktopsFacts))
    kde_desktop_installed = desktop_facts.kde_installed
    gnome_desktop_installed = desktop_facts.gnome_installed

    # No desktop installed, we don't even care about apps as they are most likely not used or even installed
    if not kde_desktop_installed and not gnome_desktop_installed:
        api.current_logger().info("No desktop installed. Continuing with the upgrade.")
        return

    if kde_desktop_installed:
        api.current_logger().info("KDE desktop is installed. Checking what we can do about it.")
        if not gnome_desktop_installed:
            api.current_logger().error("Cannot perform the upgrade because there is"
                                       " no other desktop than KDE installed.")
            # We cannot continue with the upgrade process
            reporting.create_report([
                reporting.Title("The installed KDE environment is unavailable on RHEL 8."),
                reporting.Summary(
                    "Because the KDE desktop environment is not available on RHEL 8, all the KDE-related packages"
                    " would be removed during the upgrade. There would be no desktop environment installed after the"
                    " upgrade."),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags([
                    reporting.Tags.DESKTOP
                ]),
                reporting.Flags([
                    reporting.Flags.INHIBITOR
                ]),
                reporting.Remediation(
                    hint=("Remove KDE (at least the `kde-workspace` package) or install the GNOME desktop environment"
                          " to be able to upgrade."),
                    commands=[['yum', '-y', 'groupinstall', '"Server with GUI"']])
                ])
            return

        # Assume both GNOME and KDE are installed in this state
        api.current_logger().info("Upgrade can be performed, but KDE desktop will"
                                  " be removed in favor of GNOME")
        reporting.create_report([
            reporting.Title("Upgrade can be performed, but KDE will be uninstalled."),
            reporting.Summary("The KDE desktop environment is unavailable on RHEL 8. KDE will be uninstalled "
                              "in favor of GNOME during the upgrade."),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Tags([
                reporting.Tags.DESKTOP
            ])])
        api.current_logger().info("----------------------------------")

    # At this state we just need to detect whether any KDE/Qt app is installed to inform user
    # that the application will be removed during the upgrade process. No matter if KDE is installed
    # or not.

    KDEAppsFacts = next(api.consume(InstalledKdeAppsFacts))
    if KDEAppsFacts.installed_apps:
        # upgrade can be performed, but user will loose KDE apps
        api.current_logger().info("Installed KDE/Qt apps detected.")
        reporting.create_report([
            reporting.Title("Upgrade can be performed, but KDE/Qt apps will be uninstalled."),
            reporting.Summary("The KDE desktop environment is unavailable on RHEL 8. "
                              "All the KDE/Qt apps will be removed during the upgrade, including but not limited "
                              "to:\n- {0}".format("\n- ".join(KDEAppsFacts.installed_apps))),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Tags([
                reporting.Tags.DESKTOP
            ])])
    else:
        api.current_logger().info("No KDE app in use detected.")
        # upgrade can be performed


# repos/system_upgrade/el7toel8/actors/checkkdegnome/tests/unit_test_checkkdegnome.py

from leapp.models import InstalledDesktopsFacts, InstalledKdeAppsFacts, Report


no_desktop_env = InstalledDesktopsFacts(gnome_installed=False,
                                        kde_installed=False)
gnome_desktop_env = InstalledDesktopsFacts(gnome_installed=True,
                                           kde_installed=False)
KDE_desktop_env = InstalledDesktopsFacts(gnome_installed=False,
                                         kde_installed=True)
both_desktop_env = InstalledDesktopsFacts(gnome_installed=True,
                                          kde_installed=True)


no_KDE_apps = InstalledKdeAppsFacts(installed_apps=[])
some_KDE_apps = InstalledKdeAppsFacts(installed_apps=["okular", "kate"])


def test_no_desktop_no_apps(current_actor_context):
    """
    No action expected.
    """
    current_actor_context.feed(no_desktop_env)
    current_actor_context.feed(no_KDE_apps)
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_gnome_desktop_no_apps(current_actor_context):
    """
    No action expected.
    """
    current_actor_context.feed(gnome_desktop_env)
    current_actor_context.feed(no_KDE_apps)
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_gnome_desktop_KDE_apps(current_actor_context):
    """
    One report about deleting KDE apps expected.
    """
    current_actor_context.feed(gnome_desktop_env)
    current_actor_context.feed(some_KDE_apps)
    current_actor_context.run()
    message = current_actor_context.consume(Report)[0]
    assert "Upgrade can be performed, but KDE/Qt apps will be uninstalled." in message.report["title"]


def test_KDE_desktop_no_apps(current_actor_context):
    """
    "Inhibitor" flag in report expected.
    """
    current_actor_context.feed(KDE_desktop_env)
    current_actor_context.feed(no_KDE_apps)
    current_actor_context.run()
    message = current_actor_context.consume(Report)[0]
    assert "inhibitor" in message.report["flags"]


def test_KDE_desktop_KDE_apps(current_actor_context):
    """
    "Inhibitor" flag in report expected.
    """
    current_actor_context.feed(KDE_desktop_env)
    current_actor_context.feed(some_KDE_apps)
    current_actor_context.run()
    message = current_actor_context.consume(Report)[0]
    assert "inhibitor" in message.report["flags"]


def test_both_desktops_no_apps(current_actor_context):
    """
    Report about removing KDE desktop environment expected.
    """
    current_actor_context.feed(both_desktop_env)
    current_actor_context.feed(no_KDE_apps)
    current_actor_context.run()
    message = current_actor_context.consume(Report)[0]
    assert "Upgrade can be performed, but KDE will be uninstalled." in message.report["title"]


def test_both_desktop_KDE_apps(current_actor_context):
    """
    Two reports expected, first about removing KDE desktop, second about KDE/Qt apps
    """
    current_actor_context.feed(both_desktop_env)
    current_actor_context.feed(some_KDE_apps)
    current_actor_context.run()
    messages = current_actor_context.consume(Report)
    remove_KDE_title = "Upgrade can be performed, but KDE will be uninstalled."
    remove_apps_title = "Upgrade can be performed, but KDE/Qt apps will be uninstalled."
    assert len(messages) == 2
    assert [True for message in messages if remove_KDE_title in message.report["title"]]
    assert [True for message in messages if remove_apps_title in message.report["title"]]


# repos/system_upgrade/el7toel8/actors/checkmemcached/libraries/checkmemcached.py

import re

from leapp import reporting
from leapp.libraries.stdlib import api, run


COMMON_REPORT_TAGS = [reporting.Tags.SERVICES]


sysconfig_path = '/etc/sysconfig/memcached'

related = [
    reporting.RelatedResource('package', 'memcached'),
    reporting.RelatedResource('file', sysconfig_path)
]


def is_sysconfig_default():
    """Check if the memcached sysconfig file was not modified since installation."""
    try:
        result = run(['rpm', '-V', '--nomtime', 'memcached'], checked=False)
        return sysconfig_path not in result['stdout']
    except OSError as e:
        api.current_logger().warning("rpm verification failed: %s", str(e))
        return True


def is_udp_disabled():
    """Check if UDP port is disabled in the sysconfig file."""
    with open(sysconfig_path) as f:
        for line in f:
            if re.match(r'^\s*OPTIONS=.*-U\s*0[^0-9]', line):
                return True
    return False


def check_memcached(memcached_installed):
    """Report potential issues in memcached configuration."""
    if not memcached_installed:
        api.current_logger().info('memcached package is not installed')
        return

    default_memcached_conf = is_sysconfig_default()
    disabled_udp_port = is_udp_disabled()

    if default_memcached_conf:
        reporting.create_report([
            reporting.Title('memcached service is using default configuration'),
            reporting.Summary('memcached in RHEL8 listens on loopback only and has UDP port disabled by default'),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Tags(COMMON_REPORT_TAGS),
        ] + related)

    elif not disabled_udp_port:
        reporting.create_report([
            reporting.Title('memcached has enabled UDP port'),
            reporting.Summary(
                'memcached in RHEL7 has UDP port enabled by default, but it is disabled by default in RHEL8'
            ),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Tags(COMMON_REPORT_TAGS),
        ] + related)

    else:
        reporting.create_report([
            reporting.Title('memcached has already disabled UDP port'),
            reporting.Summary('memcached in RHEL8 has UDP port disabled by default'),
            reporting.Severity(reporting.Severity.LOW),
            reporting.Tags(COMMON_REPORT_TAGS),
        ] + related)


# repos/system_upgrade/el7toel8/actors/checkmemcached/tests/unit_test_checkmemcached.py

from leapp.libraries.actor import checkmemcached
from leapp import reporting
from leapp.libraries.common.testutils import create_report_mocked


def test_uninstalled(monkeypatch):
    for sysconfig_default in (False, True):
        for udp_disabled in (False, True):
            monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
            monkeypatch.setattr(checkmemcached, 'is_sysconfig_default', lambda: sysconfig_default, )
            monkeypatch.setattr(checkmemcached, 'is_udp_disabled', lambda: udp_disabled)

            checkmemcached.check_memcached(False)

            assert reporting.create_report.called == 0


def test_installed_defconf(monkeypatch):
    for udp_disabled in (False, True):
        monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
        monkeypatch.setattr(checkmemcached, 'is_sysconfig_default', lambda: True)
        monkeypatch.setattr(checkmemcached, 'is_udp_disabled', lambda: udp_disabled)

        checkmemcached.check_memcached(True)

        assert reporting.create_report.called == 1
        assert reporting.create_report.report_fields['title'] == 'memcached service is using default configuration'


def test_installed_nodefconf_udp(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(checkmemcached, 'is_sysconfig_default', lambda: False)
    monkeypatch.setattr(checkmemcached, 'is_udp_disabled', lambda: False)

    checkmemcached.check_memcached(True)

    assert reporting.create_report.called == 1
    assert reporting.create_report.report_fields['title'] == 'memcached has enabled UDP port'


def test_installed_nodefconf_noudp(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(checkmemcached, 'is_sysconfig_default', lambda: False)
    monkeypatch.setattr(checkmemcached, 'is_udp_disabled', lambda: True)

    checkmemcached.check_memcached(True)

    assert reporting.create_report.called == 1
    assert reporting.create_report.report_fields['title'] == 'memcached has already disabled UDP port'


# repos/system_upgrade/el7toel8/actors/checkmemory/libraries/checkmemory.py

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import MemoryInfo

min_req_memory = {
    architecture.ARCH_X86_64: 1536,  # 1.5G
    architecture.ARCH_ARM64: 2048,  # 2Gb
    architecture.ARCH_PPC64LE: 2048,  # 2Gb
    architecture.ARCH_S390X: 1024  # 1Gb
}


def _check_memory(mem_info):
    msg = {}

    for arch, min_req in iter(min_req_memory.items()):
        if architecture.matches_architecture(arch):
            is_ok = mem_info.mem_total >= min_req
            msg = {} if is_ok else {'detected': mem_info.mem_total,
                                    'minimal_req': min_req}

    return msg


def process():
    memoryinfo = next(api.consume(MemoryInfo), None)
    if memoryinfo is None:
        raise StopActorExecutionError(message=("Missing information about Memory."))

    minimum_req_error = _check_memory(memoryinfo)

    if minimum_req_error:
        title = 'Minimum memory requirements for RHEL 8 are not met'
        summary = 'Memory detected: {} KiB, required: {} KiB'.format(minimum_req_error['detected'],
                                                                     minimum_req_error['minimal_req'])
        reporting.create_report([
                          reporting.Title(title),
                          reporting.Summary(summary),
                          reporting.Severity(reporting.Severity.HIGH),
                          reporting.Tags([reporting.Tags.SANITY]),
                          reporting.Flags([reporting.Flags.INHIBITOR]),
                      ])


# repos/system_upgrade/el7toel8/actors/checkmemory/tests/test_checkmemory.py

from leapp import reporting
from leapp.libraries.actor import checkmemory
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import MemoryInfo


def test_check_memory_low(monkeypatch):
    minimum_req_error = []
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    minimum_req_error = checkmemory._check_memory(MemoryInfo(mem_total=1024))
    assert minimum_req_error


def test_check_memory_high(monkeypatch):
    minimum_req_error = []
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    minimum_req_error = checkmemory._check_memory(MemoryInfo(mem_total=16273492))
    assert not minimum_req_error


def test_report(monkeypatch):
    title_msg = 'Minimum memory requirements for RHEL 8 are not met'
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(api, 'consume', lambda x: iter([MemoryInfo(mem_total=129)]))
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())
    checkmemory.process()
    assert reporting.create_report.called
    assert title_msg == reporting.create_report.report_fields['title']
    assert reporting.Flags.INHIBITOR in reporting.create_report.report_fields['flags']


# repos/system_upgrade/el7toel8/actors/checkmultiplepackageversions/libraries/checkmultiplepackageversions.py

from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRPM
from leapp.reporting import create_report, Title, Summary, Severity, Flags, Remediation, RelatedResource

# package_name: remedy information
PROBLEM_PACKAGE_MAP = {
    'brlapi.i686': {'bugzilla': None},
    'gnome-online-accounts-devel.i686': {
        'bugzilla': 'https://bugzilla.redhat.com/show_bug.cgi?id=1765627'},
    'geocode-glib-devel.i686': {
        'bugzilla': 'https://bugzilla.redhat.com/show_bug.cgi?id=1765629'}}


def check():
    actual_problems = []
    related_resources = []
    for package, details in PROBLEM_PACKAGE_MAP.items():
        name, arch = package.split('.')
        if has_package(InstalledRPM, name, arch) and has_package(InstalledRPM, name, 'x86_64'):
            actual_problems.append(package)
            # generate RelatedResources for the report
            related_resources.append(RelatedResource('package', package))
            if details['bugzilla']:
                related_resources.append(RelatedResource('bugzilla', details['bugzilla']))

    if actual_problems:
        remediation = ["yum", "remove", "-y"] + actual_problems
        # create a single report entry for all problematic packages
        create_report([
            Title('Some packages have both 32bit and 64bit version installed which are known '
                  'to cause rpm transaction test to fail'),
            Summary('The following packages have both 32bit and 64bit version installed which are known '
                    'to cause rpm transaction test to fail:\n{}'.format(
                        '\n'.join(['- {}'.format(a) for a in actual_problems]))),
            Severity(Severity.HIGH),
            Flags([Flags.INHIBITOR]),
            Remediation(commands=[remediation])] + related_resources)


# repos/system_upgrade/el7toel8/actors/checkmultiplepackageversions/tests/test_multiplepackageversions.py

import mock

from leapp.actors import Actor
from leapp.models import InstalledRPM, Report, RPM


def test_x32_x64(current_actor_context):
    problem_rpms = [
       RPM(name='brlapi', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER", arch='i686',
           pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
       RPM(name='gnome-online-accounts-devel', version='0.1', release='1.sm01', epoch='1',
           packager="RH_PACKAGER", arch='i686', pgpsig='SOME_OTHER_SIG_X'),
       RPM(name='geocode-glib-devel', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER",
           arch='i686', pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 5326810137017186'),
       RPM(name='brlapi', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER", arch='x86_64',
           pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
       RPM(name='gnome-online-accounts-devel', version='0.1', release='1.sm01', epoch='1',
           packager="RH_PACKAGER", arch='x86_64', pgpsig='SOME_OTHER_SIG_X'),
       RPM(name='geocode-glib-devel', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER",
           arch='x86_64',
           pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 5326810137017186')
       ]

    current_actor_context.feed(InstalledRPM(items=problem_rpms))
    current_actor_context.run()
    report = current_actor_context.consume(Report)[0].report
    assert report['title'] == ('Some packages have both 32bit and 64bit version installed which are known to'
                               ' cause rpm transaction test to fail')
    assert {p['title'] for p in report['detail']['related_resources'] if p['scheme'] == 'package'} == \
           {'brlapi.i686', 'gnome-online-accounts-devel.i686', 'geocode-glib-devel.i686'}


def test_1_package(current_actor_context):
    pkg = 'geocode-glib-devel'
    problem_rpms = [
       RPM(name=pkg, version='0.1', release='1.sm01', epoch='1',
           packager="RH_PACKAGER", arch='x86_64', pgpsig='SOME_OTHER_SIG_X'),
       RPM(name=pkg, version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER",
           arch='i686',
           pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 5326810137017186')
    ]
    current_actor_context.feed(InstalledRPM(items=problem_rpms))
    current_actor_context.run()
    report = current_actor_context.consume(Report)[0].report
    assert report['title'] == ('Some packages have both 32bit and 64bit version installed which are known to'
                               ' cause rpm transaction test to fail')
    assert {p['title'] for p in report['detail']['related_resources'] if p['scheme'] == 'package'} == \
           {'{}.i686'.format(pkg)}


def test_x64_only(current_actor_context):
    ok_rpms = [
       RPM(name='brlapi', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER", arch='x86_64',
           pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
       RPM(name='gnome-online-accounts-devel', version='0.1', release='1.sm01', epoch='1',
           packager="RH_PACKAGER", arch='x86_64', pgpsig='SOME_OTHER_SIG_X'),
       RPM(name='geocode-glib-devel', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER",
           arch='x86_64',
           pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 5326810137017186')
       ]

    current_actor_context.feed(InstalledRPM(items=ok_rpms))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_x32_only(current_actor_context):
    ok_rpms = [
       RPM(name='brlapi', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER", arch='i686',
           pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
       RPM(name='gnome-online-accounts-devel', version='0.1', release='1.sm01', epoch='1',
           packager="RH_PACKAGER", arch='i686', pgpsig='SOME_OTHER_SIG_X'),
       RPM(name='geocode-glib-devel', version='0.1', release='1.sm01', epoch='1', packager="RH_PACKAGER",
           arch='i686', pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 5326810137017186'),
       ]

    current_actor_context.feed(InstalledRPM(items=ok_rpms))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkntp/libraries/checkntp.py

import base64
import io
import os
import tarfile

from leapp import reporting
from leapp.libraries.stdlib import CalledProcessError, api, run
from leapp.models import NtpMigrationDecision


files = [
    '/etc/ntp.conf', '/etc/ntp/keys',
    '/etc/ntp/crypto/pw', '/etc/ntp/step-tickers'
]

related = [
    reporting.RelatedResource('package', 'ntpd'),
    reporting.RelatedResource('package', 'chrony'),
] + [reporting.RelatedResource('file', f) for f in files]


# Check if a service is active and enabled
def check_service(name):
    for state in ['active', 'enabled']:
        try:
            run(['systemctl', 'is-{}'.format(state), name])
            api.current_logger().debug('{} is {}'.format(name, state))
        except CalledProcessError:
            api.current_logger().debug('{} is not {}'.format(name, state))
            return False

    return True


# Check if a file exists
def is_file(name):
    return os.path.isfile(name)


# Get a base64-encoded gzipped tarball of specified files
def get_tgz64(filenames):
    stream = io.BytesIO()
    tar = tarfile.open(fileobj=stream, mode='w:gz')
    for filename in filenames:
        if os.path.isfile(filename):
            tar.add(filename)
    tar.close()

    return base64.b64encode(stream.getvalue())


# Check services from the ntp packages for migration
def check_ntp(installed_packages):
    service_data = [('ntpd', 'ntp', '/etc/ntp.conf'),
                    ('ntpdate', 'ntpdate', '/etc/ntp/step-tickers'),
                    ('ntp-wait', 'ntp-perl', None)]

    migrate_services = []
    migrate_configs = []
    for service, package, main_config in service_data:
        if package in installed_packages and \
                check_service('{}.service'.format(service)) and \
                (not main_config or is_file(main_config)):
            migrate_services.append(service)
            if main_config:
                migrate_configs.append(service)

    if migrate_configs:
        reporting.create_report([
            reporting.Title('{} configuration will be migrated'.format(' and '.join(migrate_configs))),
            reporting.Summary('{} service(s) detected to be enabled and active'.format(', '.join(migrate_services))),
            reporting.Severity(reporting.Severity.LOW),
            reporting.Tags([reporting.Tags.SERVICES, reporting.Tags.TIME_MANAGEMENT]),
        ] + related)

        # Save configuration files that will be renamed in the upgrade
        config_tgz64 = get_tgz64(files)
    else:
        api.current_logger().info('ntpd/ntpdate configuration will not be migrated')
        migrate_services = []
        config_tgz64 = ''

    return NtpMigrationDecision(migrate_services=migrate_services, config_tgz64=config_tgz64)


# repos/system_upgrade/el7toel8/actors/checkntp/tests/unit_test_checkntp.py

import base64
import io
import os
import re
import tarfile
import tempfile

from leapp.libraries.actor import checkntp
from leapp import reporting
from leapp.libraries.common.testutils import create_report_mocked


def test_nomigration(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(checkntp, 'check_service', lambda _: False)
    monkeypatch.setattr(checkntp, 'is_file', lambda _: False)
    monkeypatch.setattr(checkntp, 'get_tgz64', lambda _: '')

    checkntp.check_ntp(set(['chrony', 'linuxptp', 'xterm']))

    assert reporting.create_report.called == 0


def test_migration(monkeypatch):
    for packages, services, migrate in [
                (['ntp'], ['ntpd'], ['ntpd']),
                (['ntp', 'ntpdate'], ['ntpd'], ['ntpd']),
                (['ntpdate'], ['ntpdate'], ['ntpdate']),
                (['ntp', 'ntpdate'], ['ntpdate'], ['ntpdate']),
                (['ntp', 'ntpdate'], ['ntpd', 'ntpdate'], ['ntpd', 'ntpdate']),
                (['ntp', 'ntpdate', 'ntp-perl'], ['ntpd', 'ntpdate'], ['ntpd', 'ntpdate']),
                (['ntp', 'ntpdate'], ['ntpd', 'ntpdate', 'ntp-wait'], ['ntpd', 'ntpdate']),
                (['ntp', 'ntpdate', 'ntp-perl'], ['ntpd', 'ntpdate', 'ntp-wait'], ['ntpd', 'ntpdate', 'ntp-wait']),
            ]:
        monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
        monkeypatch.setattr(checkntp, 'check_service', lambda service: service[:-8] in services)
        monkeypatch.setattr(checkntp, 'is_file', lambda _: True)
        monkeypatch.setattr(checkntp, 'get_tgz64', lambda _: '')

        decision = checkntp.check_ntp(set(packages))

        assert reporting.create_report.called == 1
        assert 'configuration will be migrated' in reporting.create_report.report_fields['title']
        for service in ['ntpd', 'ntpdate']:
            migrated = re.search(r'\b{}\b'.format(service),
                                 reporting.create_report.report_fields['title']) is not None
            assert migrated == (service in migrate)

        assert decision.migrate_services == migrate


def test_tgz64(monkeypatch):
    f, name = tempfile.mkstemp()
    os.close(f)
    tgz64 = checkntp.get_tgz64([name])

    stream = io.BytesIO(base64.b64decode(tgz64))
    tar = tarfile.open(fileobj=stream, mode='r:gz')
    names = tar.getnames()

    tar.close()
    os.unlink(name)

    assert names == [name.lstrip('/')]


# repos/system_upgrade/el7toel8/actors/checkpcidrivers/libraries/checkpcidrivers.py

"""
TODOs:
1. consider the idea to compare data sources timestamp in order to decide
   if the source is actual or not
"""

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp.models import PCIDevices, RestrictedPCIDevices
from leapp.reporting import create_report


def render_report(
    restricted_driver_names_on_host,
    restricted_pci_ids_on_host,
    restricted_devices_drivers,
    restricted_devices_pcis,
    inhibit_upgrade=False,
):
    """
    Render the report for restricted PCI devices in use.

    :param inhibit_upgrade: if True, the report will have Inhibitor flag and
        the upgrade will be inhibited.
    """
    # Prefilter needed data for the report
    unavailable_driver_names = tuple(
        driver
        for driver in restricted_driver_names_on_host
        if restricted_devices_drivers[driver].available_rhel8 == 0
    )
    unavailable_pci_ids = tuple(
        pci_id
        for pci_id in restricted_pci_ids_on_host
        if restricted_devices_pcis[pci_id].available_rhel8 == 0
    )
    unsupported_driver_names = tuple(
        driver
        for driver in restricted_driver_names_on_host
        if restricted_devices_drivers[driver].supported_rhel8 == 0
    )
    unsupported_pci_ids = tuple(
        pci_id
        for pci_id in restricted_pci_ids_on_host
        if restricted_devices_pcis[pci_id].supported_rhel8 == 0
    )

    # Render the report entities
    title = "Detected PCI drivers that are restricted on RHEL8."

    # Prepare summary
    summary_partial = "The following drivers are restricted on RHEL 8 system:"
    resources = []
    # Process unavailable devices
    if unavailable_driver_names or unavailable_pci_ids:
        summary_partial += "\n\t Unavailable in RHEL8:"
        if unavailable_driver_names:
            summary_partial += "\n\t\t- driver names:\n"
            summary_partial += "\t\t\t- " + "\n\t\t\t- ".join(
                unavailable_driver_names
            )
            resources += [reporting.RelatedResource('kernel-driver', i) for i in unavailable_driver_names]
        if unavailable_pci_ids:
            summary_partial += "\n\t\t- pci ids:\n"
            summary_partial += "\t\t\t- " + "\n\t\t\t- ".join(
                unavailable_pci_ids
            )

    # Process unsupported devices
    if unsupported_driver_names or unsupported_pci_ids:
        summary_partial += "\n\t Unsupported in RHEL8:"
        if unsupported_driver_names:
            summary_partial += "\n\t\t- driver names:\n"
            summary_partial += "\t\t\t- " + "\n\t\t\t- ".join(
                unsupported_driver_names
            )
            resources += [reporting.RelatedResource('kernel-driver', i) for i in unsupported_driver_names]
        if unsupported_pci_ids:
            summary_partial += "\n\t\t- pci ids:\n"
            summary_partial += "\t\t\t- " + "\n\t\t\t- ".join(
                unsupported_pci_ids
            )

    reports = [
        reporting.Title(title),
        reporting.Summary(summary_partial),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Tags([reporting.Tags.KERNEL, reporting.Tags.DRIVERS]),
    ]

    if resources:
        reports += resources
    if inhibit_upgrade:
        reports += [reporting.Flags([reporting.Flags.INHIBITOR])]
    return reports


def create_dict_lookup(driver_names, key):
    """
    Creates a dict lookup from the list of Models.

    :param driver_names: List[leapp.models.Model[K, V]]
    :param key: a key value, that will be used as a primary key to generate a
        result Dict
    :return: Dict[leapp.models.Model[K,V].key, leapp.models.Model[K,V]]
    :raises ValueError: if there are duplicates of leapp.models.Model[K,V].key
    :raises AttributeError: if key not in K (Model attributes)

    Example:
    >>> create_dict_lookup([Model(a=1, b=2), Model(a=2, b=3)], key="b") == {
    >>>     2: Model(a=1, b=2),
    >>>     3: Model(a=2, b=3),
    >>> }
    """
    lookup = {getattr(item, key): item for item in driver_names}
    if len(lookup) != len(driver_names):
        raise ValueError("A duplicated key(s) found")
    return lookup


def checkpcidrivers_main():
    """Main entrypoint of the CheckPCIDrivers Actor."""
    try:
        pci_devs = next(api.consume(PCIDevices))
        restricted_pci_devs = next(api.consume(RestrictedPCIDevices))
    except StopIteration:
        raise StopActorExecutionError(
            message=(
                "At least one of the needed messages is empty. "
                "Required messages are PCIDevices, RestrictedPCIDevices."
            )
        )
    else:
        # get set of drivers-names/pci-ids presented on host
        driver_names_on_host = set(dev.driver for dev in pci_devs.devices)
        pci_ids_on_host = set(dev.pci_id for dev in pci_devs.devices)

        # get restricted driver_names and pci_ids dict lookups
        try:
            restricted_devices_drivers = create_dict_lookup(
                restricted_pci_devs.driver_names, key="driver_name"
            )
            restricted_devices_pcis = create_dict_lookup(
                restricted_pci_devs.pci_ids, key="pci_id"
            )
        except (AttributeError, ValueError) as err:
            raise StopActorExecutionError(message=str(err))
        # get set of restricted driver-names/pci-ids presented on host
        restricted_driver_names_on_host = driver_names_on_host & set(
            restricted_devices_drivers
        )
        restricted_pci_ids_on_host = pci_ids_on_host & set(
            restricted_devices_pcis
        )

        # check if at least one driver on host is not available on RHEL8
        if any(
            restricted_devices_drivers[driver_name].available_rhel8 == 0
            for driver_name in restricted_driver_names_on_host
        ) or any(
            restricted_devices_pcis[pci].available_rhel8 == 0
            for pci in restricted_pci_ids_on_host
        ):
            api.current_logger().critical(
                "Some of the host drivers are unavailable on the RHEL 8 "
                "system. Inhibiting the upgrade...",
            )
            create_report(
                render_report(
                    restricted_driver_names_on_host,
                    restricted_pci_ids_on_host,
                    restricted_devices_drivers,
                    restricted_devices_pcis,
                    inhibit_upgrade=True,
                )
            )
        # check if at least one driver on host is not supported on RHEL8
        elif any(
            restricted_devices_drivers[driver_name].supported_rhel8 == 0
            for driver_name in restricted_driver_names_on_host
        ) or any(
            restricted_devices_pcis[pci].supported_rhel8 == 0
            for pci in restricted_pci_ids_on_host
        ):
            api.current_logger().warning(
                "Some of the host drivers are unsupported on the RHEL 8 "
                "system. Warning the user...",
            )
            create_report(
                render_report(
                    restricted_driver_names_on_host,
                    restricted_pci_ids_on_host,
                    restricted_devices_drivers,
                    restricted_devices_pcis,
                )
            )


# repos/system_upgrade/el7toel8/actors/checkpcidrivers/tests/unit_test_checkpcidrivers.py

import itertools
import logging
from functools import partial

import pytest

from leapp import reporting
from leapp.libraries.actor.checkpcidrivers import (
    checkpcidrivers_main,
    create_dict_lookup,
    render_report,
)
from leapp.libraries.common.testutils import (
    CurrentActorMocked,
    create_report_mocked,
    produce_mocked,
)
from leapp.libraries.stdlib import api
from leapp.models import (
    Model,
    PCIDevice,
    PCIDevices,
    RestrictedPCIDevice,
    RestrictedPCIDevices,
    fields,
)
from leapp.topics.systeminfo import SystemInfoTopic

logger = logging.getLogger(__name__)

pci_devices = PCIDevices(
    devices=[
        PCIDevice(
            slot="00:00.0",
            driver="",
            dev_cls="Host bridge",
            vendor="Intel Corporation",
            name="440FX - 82441FX PMC [Natoma]",
            subsystem_vendor="Red Hat, Inc.",
            subsystem_name="Qemu virtual machine",
            pci_id="",
            rev="02",
        ),
        PCIDevice(
            slot="00:01.0",
            dev_cls="ISA bridge",
            vendor="Intel Corporation",
            name="82371SB PIIX3 ISA [Natoma/Triton II]",
            subsystem_vendor="Red Hat, Inc.",
            pci_id="15b560:0739",
            subsystem_name="Qemu virtual machine",
        ),
        PCIDevice(
            slot="00:01.1",
            dev_cls="IDE interface",
            vendor="Intel Corporation",
            name="82371SB PIIX3 IDE [Natoma/Triton II]",
            subsystem_vendor="Red Hat, Inc.",
            subsystem_name="Qemu virtual machine",
            pci_id="15b560:0739",
            progif="80",
        ),
    ]
)

restricted_pci_devices = RestrictedPCIDevices(
    driver_names=[
        RestrictedPCIDevice(
            pci_id="nan",
            driver_name="3w-9xxx",
            device_name="3w-9xxx",
            available_rhel7=1,
            supported_rhel7=1,
            available_rhel8=0,
            supported_rhel8=0,
            available_rhel9=0,
            supported_rhel9=0,
            comment="nan",
        ),
        RestrictedPCIDevice(
            pci_id="nan",
            driver_name="3w-sas",
            device_name="3w-sas",
            available_rhel7=1,
            supported_rhel7=1,
            available_rhel8=1,
            supported_rhel8=0,
            available_rhel9=0,
            supported_rhel9=0,
            comment="nan",
        ),
        RestrictedPCIDevice(
            pci_id="nan",
            driver_name="acard-ahci",
            device_name="acard-ahci",
            available_rhel7=1,
            supported_rhel7=1,
            available_rhel8=1,
            supported_rhel8=1,
            available_rhel9=0,
            supported_rhel9=0,
            comment="nan",
        ),
    ],
    pci_ids=[
        RestrictedPCIDevice(
            pci_id="0x1000:0x0060",
            driver_name="megaraid_sas",
            device_name="SAS1078R",
            available_rhel7=1,
            supported_rhel7=1,
            available_rhel8=0,
            supported_rhel8=0,
            available_rhel9=0,
            supported_rhel9=0,
            comment="nan",
        ),
        RestrictedPCIDevice(
            pci_id="0x1000:0x0064",
            driver_name="mpt2sas",
            device_name="SAS2116_1",
            available_rhel7=1,
            supported_rhel7=1,
            available_rhel8=1,
            supported_rhel8=0,
            available_rhel9=0,
            supported_rhel9=0,
            comment="nan",
        ),
        RestrictedPCIDevice(
            pci_id="0x1000:0x0065",
            driver_name="mpt2sas",
            device_name="SAS2116_2",
            available_rhel7=1,
            supported_rhel7=1,
            available_rhel8=1,
            supported_rhel8=1,
            available_rhel9=0,
            supported_rhel9=0,
            comment="nan",
        ),
    ],
)


@pytest.mark.parametrize(
    (
        "driver_name",
        "pci_id",
        "inhibit_upgrade",
        "num_msgs",
    ),
    [
        (
            "good driver name",
            "good pci id",
            None,
            0,
        ),
        (
            "3w-9xxx",
            "good pci id",
            True,
            1,
        ),
        (
            "3w-sas",
            "good pci id",
            None,
            1,
        ),
        (
            "acard-ahci",
            "good pci id",
            None,
            0,
        ),
        (
            "good driver name",
            "0x1000:0x0060",
            True,
            1,
        ),
        (
            "good driver name",
            "0x1000:0x0064",
            None,
            1,
        ),
        (
            "good driver name",
            "0x1000:0x0065",
            None,
            0,
        ),
    ],
)
def test_basic_checkpcidrivers(
    monkeypatch,
    driver_name,
    pci_id,
    inhibit_upgrade,
    num_msgs,
):
    """Check the main actor function."""
    pci_devices.devices[0].driver = driver_name
    pci_devices.devices[0].pci_id = pci_id
    monkeypatch.setattr(
        api,
        "current_actor",
        CurrentActorMocked(msgs=[pci_devices, restricted_pci_devices]),
    )
    monkeypatch.setattr(
        reporting,
        "create_report",
        create_report_mocked(),
    )
    monkeypatch.setattr(
        CurrentActorMocked,
        "produce",
        produce_mocked(),
    )
    checkpcidrivers_main()
    if inhibit_upgrade:
        assert len(CurrentActorMocked.produce.model_instances) == 1
        assert CurrentActorMocked.produce.model_instances[0].report[
            "flags"
        ] == ["inhibitor"]
    if num_msgs and not inhibit_upgrade:
        assert (
            "flags" not in CurrentActorMocked.produce.model_instances[0].report
        )
    assert len(CurrentActorMocked.produce.model_instances) == num_msgs


RestrictedPCIDevicePrefilled = partial(
    RestrictedPCIDevice,
    pci_id="",
    driver_name="",
    device_name="",
    comment="",
    available_rhel7=1,
    available_rhel9=0,
    supported_rhel7=1,
    supported_rhel9=0,
)


def get_test_case_for_testing_report(
    devices, available_rhel8, supported_rhel8
):
    return devices, {
        d: RestrictedPCIDevicePrefilled(
            available_rhel8=available_rhel8,
            supported_rhel8=supported_rhel8,
        )
        for d in devices
    }


@pytest.mark.parametrize(
    (
        "restricted_driver_names_on_host",
        "restricted_devices_drivers",
        "restricted_pci_ids_on_host",
        "restricted_devices_pcis",
        "inhibit_upgrade",
        "exp_line_length",
    ),
    [
        get_test_case_for_testing_report(("d1", "d2"), 0, 0)
        + get_test_case_for_testing_report(("p1", "p2"), 0, 0)
        + (True, 15),
        get_test_case_for_testing_report(("d1", "d2"), 1, 0)
        + get_test_case_for_testing_report(("p1", "p2"), 1, 0)
        + (True, 8),
        get_test_case_for_testing_report(("d1", "d2"), 0, 1)
        + get_test_case_for_testing_report(("p1", "p2"), 0, 1)
        + (True, 8),
        get_test_case_for_testing_report(("d1", "d2"), 1, 0)
        + get_test_case_for_testing_report(("p1", "p2"), 0, 1)
        + (True, 9),
    ],
)
def test_render_report(
    monkeypatch,
    restricted_driver_names_on_host,
    restricted_pci_ids_on_host,
    restricted_devices_drivers,
    restricted_devices_pcis,
    inhibit_upgrade,
    exp_line_length,
    caplog,
):
    """Check report appearance."""
    monkeypatch.setattr(
        reporting,
        "create_report",
        create_report_mocked(),
    )
    reporting.create_report(
        render_report(
            restricted_driver_names_on_host=restricted_driver_names_on_host,
            restricted_pci_ids_on_host=restricted_pci_ids_on_host,
            restricted_devices_drivers=restricted_devices_drivers,
            restricted_devices_pcis=restricted_devices_pcis,
            inhibit_upgrade=inhibit_upgrade,
        )
    )
    logger.info(reporting.create_report.report_fields["summary"])
    assert sum(len(m.split("\n")) for m in caplog.messages) == exp_line_length


class M(Model):
    """A model just for the purpose of further test."""

    topic = SystemInfoTopic

    a = fields.Integer(default=0)
    b = fields.Integer(default=0)


@pytest.mark.parametrize(
    ("driver_names", "key", "exp", "exception"),
    [
        # Normal case
        (
            [M(a=1, b=2), M(a=1, b=3)],
            "b",
            {2: M(a=1, b=2), 3: M(a=1, b=3)},
            None,
        ),
        # Not existing attribute
        (
            [M(a=1, b=2), M(a=1, b=3)],
            "c",
            None,
            AttributeError,
        ),
        # Not existing attribute, empty Ms
        (
            [M(), M()],
            "c",
            None,
            AttributeError,
        ),
        # Duplicated attribute values
        (
            [M(a=1, b=2), M(a=1, b=2)],
            "b",
            None,
            ValueError,
        ),
    ],
)
def test_create_dict_lookup_fn(driver_names, key, exp, exception):
    if exception:
        with pytest.raises(exception):
            create_dict_lookup(driver_names, key)
    else:
        assert create_dict_lookup(driver_names, key) == exp


def test_checkpcidrivers_no_restricted_devs(current_actor_context):
    """Test if actor works well when no messages provided."""
    current_actor_context.run()
    assert current_actor_context._messaging.errors()


# repos/system_upgrade/el7toel8/actors/checkpostfix/tests/component_test_checkpostfix.py

from leapp.models import InstalledRedHatSignedRPM, RPM
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context


RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'

with_postfix = [
    RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
    RPM(name='postfix', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

without_postfix = [
    RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
    RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_without_postfix_package(current_actor_context):
    current_actor_context.feed(create_modulesfacts(installed_rpm=without_postfix))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_actor_with_postfix_package(current_actor_context):
    current_actor_context.feed(create_modulesfacts(installed_rpm=with_postfix))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/checkremovedenvvars/libraries/checkremovedenvvars.py

from leapp.libraries.common.config import get_all_envs
from leapp.reporting import create_report
from leapp import reporting

DEPRECATED_VARS = ['LEAPP_GRUB_DEVICE']


def process():

    vars_to_report = []

    for var in get_all_envs():
        if var.name in DEPRECATED_VARS:
            vars_to_report.append(var.name)

    if vars_to_report:
        vars_str = ' '.join(vars_to_report)
        create_report([
            reporting.Title('Leapp detected removed environment variable usage'),
            reporting.Summary('The following Leapp related environment variable was removed: ' + vars_str),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Remediation(hint='Please do not use the reported variables'),
            reporting.Flags(['inhibitor']),
            reporting.Tags([reporting.Tags.UPGRADE_PROCESS]),
        ])


# repos/system_upgrade/el7toel8/actors/checkremovedenvvars/tests/test_checkremovedenvvars.py

import pytest

from leapp.libraries.common.testutils import produce_mocked
from leapp.libraries.stdlib import api
from leapp.libraries.actor import checkremovedenvvars
from leapp.libraries.common.testutils import CurrentActorMocked


def test_removed_vars(monkeypatch):
    envars = {'LEAPP_GRUB_DEVICE': '/dev/sda'}
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(envars=envars))
    monkeypatch.setattr(api.current_actor, "produce", produce_mocked())
    checkremovedenvvars.process()
    assert api.current_actor.produce.called == 1
    assert 'LEAPP_GRUB_DEVICE' in api.current_actor.produce.model_instances[0].report['summary']
    assert 'inhibitor' in api.current_actor.produce.model_instances[0].report['flags']


def test_no_removed_vars(monkeypatch):
    envars = {'LEAPP_SKIP_RHSM': '1'}
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(envars=envars))
    monkeypatch.setattr(api.current_actor, "produce", produce_mocked())
    checkremovedenvvars.process()
    assert not api.current_actor.produce.called
    assert not api.current_actor.produce.model_instances


# repos/system_upgrade/el7toel8/actors/checksaphana/libraries/checksaphana.py

from leapp.libraries.stdlib import api
from leapp.libraries.common.config import architecture
from leapp.models import SapHanaInfo
from leapp import reporting


# SAP HANA Compatibility
# Requirement is SAP HANA 2.00 rev 54 which is the minimal supported revision for both RHEL 7.9 and RHEL 8.2

SAP_HANA_MINIMAL_MAJOR_VERSION = 2
SAP_HANA_RHEL8_REQUIRED_PATCH_LEVELS = ((5, 54, 0),)
SAP_HANA_MINIMAL_VERSION_STRING = 'HANA 2.0 SPS05 rev 54 or later'


def _manifest_get(manifest, key, default_value=None):
    for entry in manifest:
        if entry.key == key:
            return entry.value
    return default_value


def running_check(info):
    """ Creates a report if a running instance of SAP HANA has been detected """
    if info.running:
        reporting.create_report([
            reporting.Title('Found running SAP HANA instances'),
            reporting.Summary(
                'In order to perform a system upgrade it is necessary that all instances of SAP HANA are stopped.'
            ),
            reporting.RemediationHint('Shutdown all SAP HANA instances before you continue with the upgrade.'),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SANITY]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.Audience('sysadmin')
        ])


def _add_hana_details(target, instance):
    """ Adds instance information into the target dictionary for creating later reports. """
    target.setdefault(instance.name, {'numbers': set(), 'path': instance.path, 'admin': instance.admin})
    target[instance.name]['numbers'].add(instance.instance_number)


def _create_detected_instances_list(details):
    """ Generates report data for detected instances in list form with details """
    result = []
    for name, meta in details.items():
        result.append(('Name: {name}\n'
                       '  Instances: {instances}\n'
                       '  Admin: {admin}\n'
                       '  Path: {path}').format(name=name,
                                                instances=', '.join(meta['numbers']),
                                                admin=meta['admin'],
                                                path=meta['path']))
    if result:
        return '- {}'.format('\n- '.join(result))
    return ''


def version1_check(info):
    """ Creates a report for SAP HANA instances running on version 1 """
    found = {}
    for instance in info.instances:
        if _manifest_get(instance.manifest, 'release') == '1.00':
            _add_hana_details(found, instance)

    if found:
        detected = _create_detected_instances_list(found)
        reporting.create_report([
            reporting.Title('Found SAP HANA 1 which is not supported with the target version of RHEL'),
            reporting.Summary(
                ('SAP HANA 1.00 is not supported with the version of RHEL you are upgrading to.\n\n'
                 'The following instances have been detected to be version 1.00:\n'
                 '{}'.format(detected))
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.RemediationHint((
                'In order to upgrade RHEL, you will have to upgrade your SAP HANA 1.0 software to '
                '{supported}.'.format(supported=SAP_HANA_MINIMAL_VERSION_STRING))),
            reporting.ExternalLink(url='https://launchpad.support.sap.com/#/notes/2235581',
                                   title='SAP HANA: Supported Operating Systems'),
            reporting.Tags([reporting.Tags.SANITY]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.Audience('sysadmin')
        ])


def _major_version_check(instance):
    """ Performs the check for the major version of SAP HANA """
    release = _manifest_get(instance.manifest, 'release', '0.00')
    parts = release.split('.')

    try:
        if int(parts[0]) != SAP_HANA_MINIMAL_MAJOR_VERSION:
            api.current_logger().info('Unsupported major version {} for instance {}'.format(release, instance.name))
            return False
        return True
    except (ValueError, IndexError):
        api.current_logger().warn(
            'Failed to parse manifest release field for instance {}'.format(instance.name), exc_info=True)
        return False


def _sp_rev_patchlevel_check(instance):
    """ Checks whether this SP, REV & PatchLevel are eligible """
    number = _manifest_get(instance.manifest, 'rev-number', '000')
    if len(number) > 2 and number.isdigit():
        required_sp_levels = [r[0] for r in SAP_HANA_RHEL8_REQUIRED_PATCH_LEVELS]
        lowest_sp = min(required_sp_levels)
        highest_sp = max(required_sp_levels)
        sp = int(number[0:2].lstrip('0') or '0')
        if sp < lowest_sp:
            # Less than minimal required SP
            return False
        if sp > highest_sp:
            # Less than minimal required SP
            return True
        for requirements in SAP_HANA_RHEL8_REQUIRED_PATCH_LEVELS:
            req_sp, req_rev, req_pl = requirements
            if sp == req_sp:
                rev = int(number.lstrip('0') or '0')
                if rev < req_rev:
                    continue
                if rev == req_rev:
                    patch_level = int(_manifest_get(instance.manifest, 'rev-patchlevel', '00').lstrip('0') or '0')
                    if patch_level < req_pl:
                        continue
                return True
        return False
    # if not 'len(number) > 2 and number.isdigit()'
    api.current_logger().warn(
        'Invalid rev-number field value `{}` in manifest for instance {}'.format(number, instance.name))
    return False


def _fullfills_hana_min_version(instance):
    """ Performs a check whether the version of SAP HANA fullfills the minimal requirements for the target RHEL """
    return _major_version_check(instance) and _sp_rev_patchlevel_check(instance)


def version2_check(info):
    """ Performs all checks for SAP HANA 2 and creates a report if anything unsupported has been detected """
    found = {}
    for instance in info.instances:
        if _manifest_get(instance.manifest, 'release', None) == '1.00':
            continue
        if not _fullfills_hana_min_version(instance):
            _add_hana_details(found, instance)

    if found:
        detected = _create_detected_instances_list(found)
        reporting.create_report([
            reporting.Title('SAP HANA needs to be updated before upgrade'),
            reporting.Summary(
                ('A newer version of SAP HANA is required in order continue with the upgrade.'
                 ' {min_hana_version} is required for the target version of RHEL.\n\n'
                 'The following SAP HANA instances have been detected to be running with a lower version'
                 ' than required on the target system:\n'
                 '{detected}').format(detected=detected, min_hana_version=SAP_HANA_MINIMAL_VERSION_STRING)
            ),
            reporting.RemediationHint('Update SAP HANA at least to {}'.format(SAP_HANA_MINIMAL_VERSION_STRING)),
            reporting.ExternalLink(url='https://launchpad.support.sap.com/#/notes/2235581',
                                   title='SAP HANA: Supported Operating Systems'),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SANITY]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.Audience('sysadmin')
        ])


def platform_check():
    """ Creates an inhibitor report in case the system is not running on x86_64 """
    if not architecture.matches_architecture(architecture.ARCH_X86_64):
        reporting.create_report([
            reporting.Title('SAP HANA upgrades are only supported on X86_64 systems'),
            reporting.Summary(
                ('Upgrades for SAP HANA are only supported on X86_64 systems.'
                 ' For more information please consult the documentation.')
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SANITY]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.Audience('sysadmin'),
            reporting.ExternalLink(
                url='https://access.redhat.com/solutions/5533441',
                title='How do I upgrade from Red Hat Enterprise Linux 7 to Red Hat Enterprise Linux 8 with SAP HANA')
        ])
        return False

    return True


def perform_check():
    """ Performs all checks for SAP HANA and will skip if the upgrade flavour is not `saphana` """

    if api.current_actor().configuration.flavour != 'saphana':
        # Do not run on non saphana upgrades
        return

    if not platform_check():
        # If this architecture is not supported, there's no sense in continuing.
        return

    info = next(api.consume(SapHanaInfo), None)
    if not info:
        return

    running_check(info)
    version1_check(info)
    version2_check(info)


# repos/system_upgrade/el7toel8/actors/checksaphana/tests/test_checksaphana.py

import pytest

from leapp.libraries.actor import checksaphana
from leapp.libraries.common import testutils
from leapp.libraries.stdlib import run
from leapp.models import SapHanaManifestEntry

SAPHANA1_MANIFEST = '''comptype: HDB
keyname: HDB
keycaption: SAP HANA DATABASE
supported-phases: prepare,offline,online
requires-restart: system
keyvendor: sap.com
release: 1.00
rev-number: 122
rev-patchlevel: 33
sp-number: 122
sp-patchlevel: 33
makeid: 7407089
date: 2020-10-08 16:23:37
platform: linuxx86_64
hdb-state: RAMP
fullversion: 1.00.122.33 Build 1602166441-1530
'''


SAPHANA2_MANIFEST = '''comptype: HDB
keyname: HDB
keycaption: SAP HANA DATABASE
supported-phases: prepare,offline,configure,online
requires-restart: system
keyvendor: sap.com
release: 2.00
rev-number: 053
rev-patchlevel: 00
sp-number: 053
sp-patchlevel: 00
max_rel2.0_sps2_rev-number: 24
max_rel2.0_sps2_patchlevel: 10
max_rel2.0_sps3_rev-number: 37
max_rel2.0_sps3_patchlevel: 07
max_rel2.0_sps4_rev-number: 48
max_rel2.0_sps4_patchlevel: 02
date: 2020-11-11 12:12:22
platform: linuxx86_64
hdb-state: RAMP
fullversion: 2.00.053.00 Build 1605092543-1530
'''


SAPHANA2_LOW_VERSION_MANIFEST = '''comptype: HDB
keyname: HDB
keycaption: SAP HANA DATABASE
supported-phases: prepare,offline,configure,online
requires-restart: system
keyvendor: sap.com
release: 2.00
rev-number: 040
rev-patchlevel: 00
sp-number: 040
sp-patchlevel: 00
max_rel2.0_sps2_rev-number: 24
max_rel2.0_sps2_patchlevel: 10
max_rel2.0_sps3_rev-number: 37
max_rel2.0_sps3_patchlevel: 07
date: 2020-11-11 12:12:22
platform: linuxx86_64
hdb-state: RAMP
fullversion: 2.00.040.00 Build 1605092543-1530
'''


def _report_has_pattern(report, pattern):
    return pattern in report[0].to_dict().get('title', '')


EXPECTED_TITLE_PATTERNS = {
    'running': lambda report: _report_has_pattern(report, 'running SAP HANA'),
    'v1': lambda report: _report_has_pattern(report, 'Found SAP HANA 1'),
    'low': lambda report: _report_has_pattern(report, 'SAP HANA needs to be updated before upgrade'),
}


def list_clear(l):
    del l[:]


def _parse_manifest_data(manifest):
    result = []
    for line in manifest.split('\n'):
        line = line.strip()
        if not line:
            continue
        # If there is a ValueError during the split, just burn with fire in this test
        key, value = line.split(': ', 1)
        result.append(SapHanaManifestEntry(key=key, value=value.strip()))
    return result


class MockSapHanaInstanceInfo(object):
    def __init__(self, name, number, path, admin, manifest_data, running=True):
        self.manifest = _parse_manifest_data(manifest_data)
        self.name = name
        self.instance_number = number
        self.path = path
        self.running = running
        self.admin = admin


def _gen_instance_info(name, manifest_data, index, running=True):
    return MockSapHanaInstanceInfo(
        name=name,
        number='{:02}'.format(index),
        path='/hana/shared/{name}/HDB{number:02}'.format(name=name, number=index),
        admin='{name}adm'.format(name=name.lower()),
        manifest_data=manifest_data,
        running=running
    )


class MockSapHanaInfo(object):
    def __init__(self, v1names, v2names, v2lownames, running=None):
        self.installed = bool(v1names or v2names or v2lownames)
        self.running = running if running is not None else self.installed
        self.instances = [_gen_instance_info(name,
                                             SAPHANA1_MANIFEST,
                                             index,
                                             running=running)
                          for index, name in enumerate(v1names)]
        self.instances += [_gen_instance_info(name,
                                              SAPHANA2_MANIFEST,
                                              index + len(v1names),
                                              running=running)
                           for index, name in enumerate(v2names)]
        self.instances += [_gen_instance_info(name,
                                              SAPHANA2_LOW_VERSION_MANIFEST,
                                              index + len(v1names) + len(v2names),
                                              running=running)
                           for index, name in enumerate(v2lownames)]


def _report_collector(reports):
    def _mock_report(args):
        reports.append(args)
    return _mock_report


def _consume_mock_sap_hana_info(v1names=(), v2names=(), v2lownames=(), running=True):
    def _consume(*models):
        return iter([MockSapHanaInfo(v1names, v2names, v2lownames, running=running)])
    return _consume


class MockSAPHanaVersionInstance(object):
    def __init__(self, major, rev, patchlevel):
        self.name = "TestName"

        def _kv(k, v):
            return SapHanaManifestEntry(key=k, value=v)
        self.manifest = [
            _kv('release', '{major}.00'.format(major=major)),
            _kv('rev-number', '{rev:03}'.format(rev=rev)),
            _kv('rev-patchlevel', '{pl:02}'.format(pl=patchlevel)),
            _kv('sp-number', '{rev:03}'.format(rev=rev)),
            _kv('sp-patchlevel', '{pl:02}'.format(pl=patchlevel)),
        ]


@pytest.mark.parametrize(
    'major,rev,patchlevel,result', (
        (2, 52, 0, True),
        (2, 52, 1, True),
        (2, 52, 2, True),
        (2, 53, 0, True),
        (2, 60, 0, True),
        (2, 48, 2, True),
        (2, 48, 1, False),
        (2, 48, 0, False),
        (2, 38, 2, False),
        (2, 49, 0, True),
    )
)
def test_checksaphana__fullfills_hana_min_version(monkeypatch, major, rev, patchlevel, result):
    monkeypatch.setattr(checksaphana, 'SAP_HANA_RHEL8_REQUIRED_PATCH_LEVELS', ((4, 48, 2), (5, 52, 0)))
    assert checksaphana._fullfills_hana_min_version(
        MockSAPHanaVersionInstance(
            major=major,
            rev=rev,
            patchlevel=patchlevel,
        )
    ) == result


def test_checksaphana_perform_check(monkeypatch):
    v1names = ('ABC', 'DEF', 'GHI')
    v2names = ('JKL', 'MNO', 'PQR', 'STU')
    v2lownames = ('VWX', 'YZA')
    reports = []
    monkeypatch.setattr(checksaphana, 'SAP_HANA_RHEL8_REQUIRED_PATCH_LEVELS', ((4, 48, 2), (5, 52, 0)))
    monkeypatch.setattr(checksaphana.reporting, 'create_report', _report_collector(reports))
    monkeypatch.setattr(checksaphana.api, 'consume', _consume_mock_sap_hana_info(
        v1names=v1names, v2names=v2names, v2lownames=v2lownames, running=True))

    for arch in (testutils.architecture.ARCH_PPC64LE,
                 testutils.architecture.ARCH_ARM64,
                 testutils.architecture.ARCH_S390X):
        for flavour in ('default', 'saphana'):
            list_clear(reports)
            monkeypatch.setattr(checksaphana.api,
                                'current_actor',
                                testutils.CurrentActorMocked(arch=arch, flavour=flavour))
            checksaphana.perform_check()
            if flavour == 'saphana':
                assert reports and len(reports) == 1
                assert 'X86_64' in reports[0][0].to_dict()['title']
            else:
                assert not reports

    list_clear(reports)
    monkeypatch.setattr(checksaphana.api,
                        'current_actor',
                        testutils.CurrentActorMocked(arch=testutils.architecture.ARCH_X86_64))
    checksaphana.perform_check()
    assert not reports

    monkeypatch.setattr(checksaphana.api,
                        'current_actor',
                        testutils.CurrentActorMocked(arch=testutils.architecture.ARCH_X86_64, flavour='saphana'))
    checksaphana.perform_check()
    assert reports
    # Expected 3 reports due to v1names + v2lownames + running
    assert len(reports) == 3
    # Verifies that all expected title patterns are within the reports and not just coincidentally 3
    assert all([any([pattern(report) for report in reports]) for pattern in EXPECTED_TITLE_PATTERNS.values()])

    list_clear(reports)
    monkeypatch.setattr(checksaphana.api, 'consume', _consume_mock_sap_hana_info(
        v1names=v1names, v2names=v2names, v2lownames=v2lownames, running=False))
    checksaphana.perform_check()
    assert reports
    # Expected 2 reports due to v1names + v2lownames
    assert len(reports) == 2
    # Verifies that all expected title patterns are within the reports and not just coincidentally 2
    assert all([any([EXPECTED_TITLE_PATTERNS[pattern](report) for report in reports]) for pattern in ['v1', 'low']])


# repos/system_upgrade/el7toel8/actors/checksendmail/libraries/checksendmail.py

import os
import re

SendmailConfDir = '/etc/mail'
SendmailConfFiles = ['sendmail.cf', 'sendmail.mc', 'submit.cf', 'submit.mc']
# false positives blacklist
rfp = re.compile(r'(^\s*RIPv6:::1\b)|(@\s+\[IPv6:::1\]\s+>)')


def get_conf_files():
    conf_files = [os.path.join(SendmailConfDir, f) for f in SendmailConfFiles]
    return conf_files


def check_false_positives(filename, line):
    return filename in ['sendmail.cf', 'submit.cf'] and rfp.search(line) is not None


def check_files_for_compressed_ipv6():
    conf_files = get_conf_files()
    migrate_files = []
    files = [os.path.join(SendmailConfDir, re.sub(r'\.db$', '', f)) for f in os.listdir(SendmailConfDir)
             if f.endswith('.db')] + conf_files
    regex = re.compile(r'IPv6:[0-9a-fA-F:]*::')
    for filename in files:
        if not os.path.exists(filename):
            continue
        with open(filename) as file_check:
            for line in file_check:
                if regex.search(line) and not check_false_positives(os.path.basename(filename), line):
                    migrate_files.append(filename)
                    break
    return migrate_files


# repos/system_upgrade/el7toel8/actors/checksendmail/tests/component_test_checksendmail.py

from leapp.models import (
    DaemonList,
    InstalledRedHatSignedRPM,
    RPM,
    SendmailMigrationDecision,
    TcpWrappersFacts,
)
from leapp.reporting import Report


RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'

with_sendmail = [
    RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
    RPM(name='sendmail', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

without_sendmail = [
    RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
    RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_without_sendmail_package(current_actor_context):
    tcpwrap_facts = TcpWrappersFacts(daemon_lists=[])
    current_actor_context.feed(create_modulesfacts(installed_rpm=without_sendmail))
    current_actor_context.feed(tcpwrap_facts)
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_actor_with_tcp_wrappers(current_actor_context):
    tcpwrap_facts = TcpWrappersFacts(daemon_lists=[DaemonList(value=['sendmail'])])
    current_actor_context.feed(create_modulesfacts(installed_rpm=with_sendmail))
    current_actor_context.feed(tcpwrap_facts)
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report
    assert 'inhibitor' in report_fields['flags']


# repos/system_upgrade/el7toel8/actors/checksendmail/tests/unit_test_checksendmail.py

import os

import pytest
from six import text_type

from leapp.libraries.actor import checksendmail


@pytest.mark.parametrize('test_input,migrate', [
    ('IPv6:::1\n', True),
    ('IPv6:0:0:0:0:0:0:0:1\n', False),
])
def test_check_migration(tmpdir, monkeypatch, test_input, migrate):
    test_cfg_path = text_type(tmpdir)
    test_cfg_file = os.path.join(test_cfg_path, 'sendmail.cf')
    with open(test_cfg_file, 'w') as file_out:
        file_out.write(test_input)
    monkeypatch.setattr(checksendmail, 'SendmailConfDir', test_cfg_path)
    files = checksendmail.check_files_for_compressed_ipv6()
    if migrate:
        assert files == [test_cfg_file]
    else:
        assert files == []


# repos/system_upgrade/el7toel8/actors/checkwireshark/tests/component_test_checkwireshark.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import InstalledRedHatSignedRPM, RPM
from leapp.reporting import Report

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'


def test_actor_with_grep_package(current_actor_context):
    rpms = [
        RPM(name='wireshark', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(InstalledRedHatSignedRPM(items=rpms))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


def test_actor_without_grep_package(current_actor_context):
    rpms = [
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(InstalledRedHatSignedRPM(items=rpms))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/cupscheck/libraries/cupscheck.py

from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.models import CupsChangedFeatures


def _get_input_model(model):
    """
    Gets data model from an actor.

    :param obj model: object of model which data will be consumed
    """
    return next(api.consume(model), None)


def check_interface_scripts(facts, report_func):
    """
    Checks if the data model tells interface scripts are used
    and produces a report.

    :param obj facts: model object containing info about CUPS configuration
    :param func report_func: creates report
    """
    title = ('CUPS no longer supports usage of interface scripts')
    summary = ('Interface scripts are no longer supported due to '
               'security issues - an attacker could provide '
               'malicious script which will be run during printing.')
    hint = ('Install the queue with PPD file for the printer '
            'if available or install the queue with generic PPD, '
            'add *cupsFilter2 directive into PPD of installed '
            'queue (in /etc/cups/ppd) and reinstall the queue with modified PPD. '
            'The interface script needs to have permissions 750 and '
            'ownership root:lp. How to write *cupsFilter2 keyword '
            'is described at https://www.cups.org/doc/spec-ppd.html#cupsFilter2 '
            'and the script needs to be put into /usr/lib/cups/filter '
            'or you need to use an absolute path to the script '
            'in *cupsFilter2 directive.')
    if facts.interface:
        args = [
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Tags([reporting.Tags.DRIVERS]),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Remediation(hint=hint),
            reporting.ExternalLink(
                title='Upstream documentation for the cupsFilter2 PPD keyword',
                url='https://www.cups.org/doc/spec-ppd.html#cupsFilter2'
            )
        ]

        report_func(args)


def check_include_directive(facts, report_func):
    """
    Checks if the data model tells include directives are used
    and produces a report.

    :param obj facts: model object containing info about CUPS configuration
    :param func report_func: creates report
    """
    title = ('CUPS no longer supports usage of Include directive')
    summary = ('Include directive was removed due to security reasons. '
               'Contents of found included files will be appended to '
               'cupsd.conf')
    if facts.include:
        args = [
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Tags([reporting.Tags.SERVICES]),
            reporting.Severity(reporting.Severity.MEDIUM),
        ] + [reporting.RelatedResource('file', f) for f in facts.include_files]

        report_func(args)


def check_printcap_directive(facts, report_func):
    """
    Checks if the data model tells printcapformat directive is used
    and produces a report.

    :param obj facts: model object containing info about CUPS configuration
    :param func report_func: creates report
    """
    title = ('PrintcapFormat directive is no longer in cupsd.conf')
    summary = (
        'The directive was moved into /etc/cups/cups-files.conf '
        'because it is deprecated. This will be handled automatically during '
        'the upgrade process.'
    )
    if facts.printcap:
        args = [
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Tags([reporting.Tags.SERVICES]),
            reporting.Severity(reporting.Severity.LOW),
            reporting.RelatedResource('file', '/etc/cups/cupsd.conf'),
            reporting.RelatedResource('file', '/etc/cups/cups-files.conf')
        ]

        report_func(args)


def check_env_directives(facts, report_func):
    """
    Checks if the data model tells PassEnv/SetEnv directives are used
    and produces a report.

    :param obj facts: model object containing info about CUPS configuration
    :param func report_func: creates report
    """
    title = ('PassEnv/SetEnv directives are no longer in cupsd.conf')
    summary = (
        'The directives were moved into /etc/cups/cups-files.conf '
        'due to security reasons. '
        'This will be handled automatically during the upgrade process.'
    )
    if facts.env:
        args = [
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Tags([reporting.Tags.SERVICES]),
            reporting.Severity(reporting.Severity.LOW),
            reporting.RelatedResource('file', '/etc/cups/cupsd.conf'),
            reporting.RelatedResource('file', '/etc/cups/cups-files.conf')
        ]

        report_func(args)


def check_certkey_directives(facts, report_func):
    """
    Checks if the data model tells ServerKey/ServerCertificate directives
    are used and produces a report.

    :param obj facts: model object containing info about CUPS configuration
    :param func report_func: creates report
    """
    title = ('ServerKey/ServerCertificate directives are substitued '
             'by ServerKeychain directive')
    summary = (
        'The directives were substitued by ServerKeychain directive, '
        'which now takes a directory as value (/etc/cups/ssl is default). '
        'The previous directives took a file as value. '
        'The migration script will copy the files specified in '
        'directive values into /etc/cups/ssl directory '
        'if they are not there already. '
        'This will be handled automatically during the upgrade process.'
    )
    if facts.certkey:
        args = [
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Tags([reporting.Tags.SERVICES,
                            reporting.Tags.AUTHENTICATION]),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.RelatedResource('file', '/etc/cups/cups-files.conf')
        ]

        report_func(args)


def check_digest_values(facts, report_func):
    """
    Checks if the data model tells Digest/BasicDigest values
    of AuthType/DefaultAuthType directives are used
    and produces a report.

    :param obj facts: model object containing info about CUPS configuration
    :param func report_func: creates report
    """
    title = ('CUPS no longer supports Digest and BasicDigest '
             'directive values')
    summary = (
        'Digest and BasicDigest directive values were removed '
        'due to deprecation. '
        'The Basic authentication with TLS encryption will be '
        'set automatically during the upgrade process. '
        'The version of the used TLS is by default dependent on the set system '
        'crypto policies.'
    )
    # NOTE: the remediation instructions are missing as we do not have any
    # doc covering that, mainly because of this is expected to be very rare
    # at all. People usually do not use Digest & BasicDigest.
    if facts.digest:
        args = [
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Tags([
                reporting.Tags.AUTHENTICATION,
                reporting.Tags.SECURITY,
                reporting.Tags.SERVICES,
            ]),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.RelatedResource('file', '/etc/cups/cupsd.conf')
        ]

        report_func(args)


def make_reports(consume_function=_get_input_model,
                 report_func=reporting.create_report,
                 debug_log=api.current_logger().debug):
    """
    Creates reports if needed

    :param func consume_function: gets data model from an actor
    :param func report_func: creates report
    """
    facts = consume_function(CupsChangedFeatures)

    if facts:
        check_interface_scripts(facts, report_func)
        check_include_directive(facts, report_func)
        check_printcap_directive(facts, report_func)
        check_env_directives(facts, report_func)
        check_certkey_directives(facts, report_func)
        check_digest_values(facts, report_func)
    else:
        debug_log('No facts gathered about CUPS - skipping reports.')


# repos/system_upgrade/el7toel8/actors/cupscheck/tests/test_check_certkey.py

import pytest

from leapp import reporting
from leapp.libraries.actor.cupscheck import check_certkey_directives
from leapp.libraries.common.testutils import create_report_mocked
from leapp.models import CupsChangedFeatures


@pytest.mark.parametrize("certkey_exists,n_reports", [(False, 0), (True, 1)])
def test_check_certkey_directives(certkey_exists, n_reports):
    facts = CupsChangedFeatures(certkey=certkey_exists)
    report_func = create_report_mocked()

    check_certkey_directives(facts, report_func)

    assert report_func.called == n_reports

    if report_func.called:
        report_fields = report_func.report_fields

        assert 'ServerKey/ServerCertificate directives' in report_fields['title']
        assert report_fields['severity'] == reporting.Severity.MEDIUM


# repos/system_upgrade/el7toel8/actors/cupscheck/tests/test_check_digest_values.py

import pytest

from leapp import reporting
from leapp.libraries.actor.cupscheck import check_digest_values
from leapp.libraries.common.testutils import create_report_mocked
from leapp.models import CupsChangedFeatures


@pytest.mark.parametrize("digest_exists,n_reports", [(False, 0), (True, 1)])
def test_check_digest_values(digest_exists, n_reports):
    facts = CupsChangedFeatures(digest=digest_exists)
    report_func = create_report_mocked()

    check_digest_values(facts, report_func)

    assert report_func.called == n_reports

    if report_func.called:
        report_fields = report_func.report_fields

        assert 'no longer supports Digest' in report_fields['title']
        assert report_fields['severity'] == reporting.Severity.MEDIUM


# repos/system_upgrade/el7toel8/actors/cupscheck/tests/test_check_env_directives.py

import pytest

from leapp import reporting
from leapp.libraries.actor.cupscheck import check_env_directives
from leapp.libraries.common.testutils import create_report_mocked
from leapp.models import CupsChangedFeatures


@pytest.mark.parametrize("env_exists,n_reports", [(False, 0), (True, 1)])
def test_check_env_directives(env_exists, n_reports):
    facts = CupsChangedFeatures(env=env_exists)
    report_func = create_report_mocked()

    check_env_directives(facts, report_func)

    assert report_func.called == n_reports

    if report_func.called:
        report_fields = report_func.report_fields

        assert 'PassEnv/SetEnv directives are no longer' in report_fields['title']
        assert report_fields['severity'] == reporting.Severity.LOW


# repos/system_upgrade/el7toel8/actors/cupscheck/tests/test_check_include_directive.py

import pytest

from leapp import reporting
from leapp.libraries.actor.cupscheck import check_include_directive
from leapp.libraries.common.testutils import create_report_mocked
from leapp.models import CupsChangedFeatures


@pytest.mark.parametrize("include_exists,n_reports", [(False, 0), (True, 1)])
def test_check_include_directive(include_exists, n_reports):
    facts = CupsChangedFeatures(include=include_exists,
                                include_files=['/etc/cups/cupsd.conf'])
    report_func = create_report_mocked()

    check_include_directive(facts, report_func)

    assert report_func.called == n_reports

    if report_func.called:
        report_fields = report_func.report_fields

        assert 'no longer supports usage of Include' in report_fields['title']
        assert report_fields['severity'] == reporting.Severity.MEDIUM


# repos/system_upgrade/el7toel8/actors/cupscheck/tests/test_check_interface_scripts.py

import pytest

from leapp import reporting
from leapp.libraries.actor.cupscheck import check_interface_scripts
from leapp.libraries.common.testutils import create_report_mocked
from leapp.models import CupsChangedFeatures


@pytest.mark.parametrize("interface_exists,n_reports", [(False, 0), (True, 1)])
def test_check_interface_scripts(interface_exists, n_reports):
    facts = CupsChangedFeatures(interface=interface_exists)
    report_func = create_report_mocked()

    check_interface_scripts(facts, report_func)

    assert report_func.called == n_reports

    if report_func.called:
        report_fields = report_func.report_fields

        assert 'usage of interface scripts' in report_fields['title']
        assert 'Interface scripts are no longer' in report_fields['summary']
        assert report_fields['severity'] == reporting.Severity.MEDIUM
        assert all('*cupsFilter2' in r['remediations']['context']
                   for r in report_fields['remediations'])


# repos/system_upgrade/el7toel8/actors/cupscheck/tests/test_check_printcap_directive.py

import pytest

from leapp import reporting
from leapp.libraries.actor.cupscheck import check_printcap_directive
from leapp.libraries.common.testutils import create_report_mocked
from leapp.models import CupsChangedFeatures


@pytest.mark.parametrize("printcap_exists,n_reports", [(False, 0), (True, 1)])
def test_check_printcap_directive(printcap_exists, n_reports):
    facts = CupsChangedFeatures(printcap=printcap_exists)
    report_func = create_report_mocked()

    check_printcap_directive(facts, report_func)

    assert report_func.called == n_reports

    if report_func.called:
        report_fields = report_func.report_fields

        assert 'PrintcapFormat directive' in report_fields['title']
        assert report_fields['severity'] == reporting.Severity.LOW


# repos/system_upgrade/el7toel8/actors/cupscheck/tests/test_make_reports.py

import pytest

from leapp.libraries.actor.cupscheck import make_reports


class MockLogger(object):
    def __init__(self):
        self.debug_msg = ''

    def debug_log(self, msg):
        self.debug_msg += msg


class MockInputFacts(object):
    def __init__(self, facts):
        self.facts = facts

    def get_facts(self, model):
        ret = None
        if model == 'CupsChangedFeatures':
            ret = self.facts

        return ret


class MockReport(object):
    def __init__(self):
        self.report = []

    # unused, report testing will be done separately
    def create_report(self, data_list):
        if data_list:
            self.report.append(data_list)


def test_make_reports():

    logger = MockLogger()
    facts = MockInputFacts(None)
    reporting = MockReport()

    make_reports(facts.get_facts, reporting.create_report, logger.debug_log)

    assert logger.debug_msg == 'No facts gathered about CUPS - skipping reports.'


# repos/system_upgrade/el7toel8/actors/cupsfiltersmigrate/libraries/cupsfiltersmigrate.py

from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM

# rpm : the default config file
BROWSED_CONFIG = '/etc/cups/cups-browsed.conf'


# The list of macros that should be set to get the behavior
# from previous RHEL
NEW_MACROS = [
    ('LocalQueueNamingRemoteCUPS', 'RemoteName'),
    ('CreateIPPPrinterQueues', 'All')
]


def _macro_exists(path, macro):
    """
    Check if macro is in the file.

    :param str path: string representing the full path of the config file
    :param str macro: new directive to be added
    :return boolean res: macro does/does not exist in the file
    """
    with open(path, 'r') as f:
        lines = f.readlines()

    for line in lines:
        if line.lstrip().startswith(macro):
            return True
    return False


def _append_string(path, content):
    """
    Append string at the end of file.

    :param str path: string representing the full path of file
    :param str content: preformatted string to be added
    """
    with open(path, 'a') as f:
        f.write(content)


def update_config(path, check_function=_macro_exists,
                  append_function=_append_string):
    """
    Insert expected content into the file on the path if it is not
    in the file already.

    :param str path: string representing the full path of the config file
    :param func check_function: function to be used to check if string is in the file
    :param func append_function: function to be used to append string
    """

    macros = []
    for macro in NEW_MACROS:
        if not check_function(path, macro[0]):
            macros.append(' '.join(macro))

    if not macros:
        return

    fmt_input = "\n{comment_line}\n{content}\n".format(comment_line='# content added by Leapp',
                                                       content='\n'.join(macros))

    try:
        append_function(path, fmt_input)
    except IOError:
        raise IOError('Error during writing to file: {}.'.format(path))


def _check_package(pkg):
    """
    Checks if a package is installed and signed

    :param str pkg: name of package
    """
    return has_package(InstalledRedHatSignedRPM, pkg)


def update_cups_browsed(debug_log=api.current_logger().debug,
                        error_log=api.current_logger().error,
                        is_installed=_check_package,
                        append_function=_append_string,
                        check_function=_macro_exists):
    """
    Update cups-browsed configuration file

    :param func debug_log: function for debug logging
    :param func error_log: function for error logging
    :param func is_installed: checks if the package is installed
    :param func append_function: appends string into file
    :param func check_function: checks if macro is in the file
    """

    error_list = []

    if not is_installed('cups-filters'):
        return

    debug_log('Updating cups-browsed configuration file {}.'
              .format(BROWSED_CONFIG))

    try:
        update_config(BROWSED_CONFIG,
                      check_function,
                      append_function)
    except (OSError, IOError) as error:
        error_list.append((BROWSED_CONFIG, error))
    if error_list:
        error_log('The files below have not been modified '
                  '(error message included):' +
                  ''.join(['\n    - {}: {}'.format(err[0], err[1])
                          for err in error_list]))
        return


# repos/system_upgrade/el7toel8/actors/cupsfiltersmigrate/tests/test_update_config_cupsfiltersmigrate.py

import pytest

from leapp.libraries.actor.cupsfiltersmigrate import NEW_MACROS, update_config


def _gen_append_str(list_out=None):
    """
    Just helper function to generate string expected to be added for an input (see testdata) for testing.

    :param list list_out: None, [0], [1], [0,1] - no more expected vals,
                          which represents what macros should be appended
                          in output
    """
    if not list_out:
        return ''
    _out_list = ('LocalQueueNamingRemoteCUPS RemoteName', 'CreateIPPPrinterQueues All')
    output = ['# content added by Leapp']
    for i in list_out:
        output.append(_out_list[i])
    # ensure the extra NL is before the string and the empty NL is in the end
    # of the string (/file) as well
    return '\n{}\n'.format('\n'.join(output))


testdata = (
    ('\n',
        _gen_append_str([0, 1])),
    ('bleblaba\n',
        _gen_append_str([0, 1])),
    ('fdnfdf\n# LocalQueueNamingRemoteCUPS RemoteName\n',
        _gen_append_str([0, 1])),
    ('fdnfdf\nfoo # LocalQueueNamingRemoteCUPS RemoteName\n',
        _gen_append_str([0, 1])),
    ('fdnfdf\n# LocalQueueNamingRemoteCUPS Bar\n',
        _gen_append_str([0, 1])),
    ('fdnfdf\n  # LocalQueueNamingRemoteCUPS Bar\n',
        _gen_append_str([0, 1])),
    ('fdnfdf\nLocalQueueNamingRemoteCUPS RemoteName\n',
        _gen_append_str([1])),
    ('fdnfdf\n  LocalQueueNamingRemoteCUPS RemoteName\n',
        _gen_append_str([1])),
    ('fdnfdf\nLocalQueueNamingRemoteCUPS Bar\n',
        _gen_append_str([1])),
    ('fnfngbfg\nCreateIPPPrinterQueues All\n',
        _gen_append_str([0])),
    ('fnfngbfg\nCreateIPPPrinterQueues Foo\n',
        _gen_append_str([0])),
    ('fnfngbfg\n  CreateIPPPrinterQueues Foo\n',
        _gen_append_str([0])),
    ('CreateIPPPrinterQueues All\nLocalQueueNamingRemoteCUPS RemoteName\n',
        _gen_append_str()),
    ('CreateIPPPrinterQueues Foo\nLocalQueueNamingRemoteCUPS Bar\n',
        _gen_append_str()),
    ('foo\nCreateIPPPrinterQueues Foo\nLocalQueueNamingRemoteCUPS Bar\nFoobar\n',
        _gen_append_str()),
    ('foo\nCreateIPPPrinterQueues Foo\n# LocalQueueNamingRemoteCUPS Bar\nFoobar\n',
        _gen_append_str([0]))
)


class MockFile(object):
    def __init__(self, path, content=None):
        self.path = path
        self.content = content
        self.error = False

    def append(self, path, content):
        if path != self.path:
            self.error = True
        if not self.error:
            self.content += content
            return self.content
        raise IOError('Error during writing to file: {}.'.format(path))

    def exists(self, path, macro):
        for line in self.content.split('\n'):
            if line.lstrip().startswith(macro) and self.path == path:
                return True
        return False


def test_update_config_file_errors():
    path = 'foo'

    f = MockFile(path, content='')

    with pytest.raises(IOError):
        update_config('bar', f.exists, f.append)

    assert f.content == ''


@pytest.mark.parametrize('content,expected', testdata)
def test_update_config_append_into_file(content, expected):
    path = 'bar'
    f = MockFile(path, content)

    update_config(path, f.exists, f.append)

    assert f.content == content + expected


# repos/system_upgrade/el7toel8/actors/cupsfiltersmigrate/tests/test_update_cups_browsed_cupsfiltersmigrate.py

import pytest

from leapp.libraries.actor.cupsfiltersmigrate import (
    BROWSED_CONFIG,
    update_cups_browsed,
)

testdata = [
    {'cups-filters': '/etc/cups/cups-browsed.conf'},
    {'cups-filters': ''},
    {'ble': ''}
]


class MockLogger(object):
    def __init__(self):
        self.debugmsg = ''
        self.errmsg = ''

    def debug(self, message):
        self.debugmsg += message

    def error(self, message):
        self.errmsg += message


class MockPackage(object):
    def __init__(self, name, config):
        self.name = name
        self.config = config
        self.config_content = ''


class MockPackageSet(object):
    def __init__(self):
        self.installed_packages = None

    def add_packages(self, pkgs):
        if self.installed_packages is None:
            self.installed_packages = []

        for rpm, config in pkgs.items():
            self.installed_packages.append(MockPackage(rpm, config))

    def is_installed(self, pkg):
        for rpm in self.installed_packages:
            if pkg == rpm.name:
                return True
        return False

    def append_content(self, path, content):
        found = False

        for rpm in self.installed_packages:
            if path == rpm.config:
                found = True
                rpm.config_content += content
        if not found:
            raise IOError('Error during writing to file: {}.'.format(path))

    def check_content(self, path, content):
        found = False

        for rpm in self.installed_packages:
            if path == rpm.config and content in rpm.config_content:
                found = True

        return found


class ExpectedOutput(object):
    def __init__(self):
        self.debugmsg = ''
        self.errmsg = ''

    def create(self, rpms):
        error_list = []

        for pkg, config in rpms.items():
            if pkg == 'cups-filters':
                self.debugmsg += 'Updating cups-browsed configuration file {}.'.format(BROWSED_CONFIG)
                if config == '':
                    error_list.append((BROWSED_CONFIG, 'Error during '
                                       'writing to file: {}.'.format(BROWSED_CONFIG)))

        if error_list:
            self.errmsg = ('The files below have not been modified '
                           '(error message included):' +
                           ''.join(['\n    - {}: {}'.format(err[0], err[1])
                                   for err in error_list]))


@pytest.mark.parametrize("rpms", testdata)
def test_update_cups_browsed(rpms):
    logger = MockLogger()
    installed_packages = MockPackageSet()

    installed_packages.add_packages(rpms)

    expected = ExpectedOutput()
    expected.create(rpms)

    update_cups_browsed(logger.debug,
                        logger.error,
                        installed_packages.is_installed,
                        installed_packages.append_content,
                        installed_packages.check_content)

    assert expected.debugmsg == logger.debugmsg
    assert expected.errmsg == logger.errmsg


# repos/system_upgrade/el7toel8/actors/cupsmigrate/libraries/cupsmigrate.py

import os
from shutil import copy

from leapp.libraries.stdlib import api
from leapp.models import CupsChangedFeatures

CUPSD_CONF = '/etc/cups/cupsd.conf'
CUPSFILES_CONF = '/etc/cups/cups-files.conf'
"""
CUPS configuration files
"""


class FileOperations(object):
    def readlines(self, path):
        if os.path.exists(path):
            with open(path, 'r') as f:
                return f.readlines()
        else:
            raise IOError('Error when reading file {} - file '
                          'does not exist.'.format(path))

    def write(self, path, mode, content):
        if isinstance(content, list):
            content = ''.join(content)
        with open(path, mode) as f:
            f.write(content)

    def copy_to_ssl(self, oldpath):
        copy(oldpath, '/etc/cups/ssl')


def migrate_digest(op):
    """
    Replaces Digest/BasicDigest for Basic

    :param obj op: file operations object
    """
    try:
        lines = op.readlines(CUPSD_CONF)
    except IOError as error:
        raise IOError(error)

    for line in lines:
        for directive in ['AuthType', 'DefaultAuthType']:
            if line.lstrip().startswith(directive):
                auth_line_value = line.lstrip().lstrip(directive).lstrip()
                for value in ['Digest', 'BasicDigest']:
                    if auth_line_value.startswith(value):
                        lines[lines.index(line)] = '{} Basic\n'.format(directive)

    op.write(CUPSD_CONF, 'w', lines)


def migrate_include(include_files, op):
    """
    Concatenates configuration files and remove lines
    with 'Include' directive

    :param list include_files: list of files which contents will be
    concatenated
    :param obj op: file operations object
    """
    error_list = []
    lines = []
    content = []

    for f in include_files:
        try:
            content = op.readlines(f)
            if f != CUPSD_CONF:
                content = ['\n# added by Leapp\n'] + content
            lines += content
        except IOError as error:
            error_list.append('Include directive: {}'.format(error))

    if error_list:
        return error_list

    for line in lines:
        if line.lstrip().startswith('Include'):
            lines[lines.index(line)] = ''

    op.write(CUPSD_CONF, 'w', lines)

    return None


def move_directives(directives, op):
    """
    Moves the directives from cupsd.conf to cups-files.conf

    :param list directives: list of wanted directives
    :param obj op: file operations object
    """
    try:
        cupsd_lines = op.readlines(CUPSD_CONF)
    except IOError as error:
        raise IOError(error)

    lines_to_move = []
    for line in cupsd_lines:
        for name in directives:
            if line.lstrip().startswith(name):
                lines_to_move.append(line)
                cupsd_lines[cupsd_lines.index(line)] = ''

    op.write(CUPSD_CONF, 'w', cupsd_lines)

    if lines_to_move:
        op.write(CUPSFILES_CONF, 'a',
                 '\n# added by Leapp\n{}'.format(''.join(lines_to_move)))


def migrate_certkey(op):
    """
    Copies the key and the certificate to /etc/cups/ssl if both
    are in different dirs, or sets ServerKeychain value to the dir
    where the key and the certificate are. Removes old directives

    :param list directives: list of wanted directives
    :param obj op: file operations object
    """
    try:
        lines = op.readlines(CUPSFILES_CONF)
    except IOError as error:
        raise IOError(error)

    certkey_values = []

    for line in lines:
        for name in ['ServerKey', 'ServerCertificate']:
            if line.lstrip().startswith(name):
                value = line.split()[1]
                if value.startswith('ssl'):
                    value = os.path.join('/etc/cups', value)
                certkey_values.append(value)
                lines[lines.index(line)] = ''

    op.write(CUPSFILES_CONF, 'w', lines)

    # we need to decide whether we copy the files to /etc/cups/ssl
    # or set ServerKeychain to non-default directory or do nothing
    if all(os.path.dirname(val) == '/etc/cups/ssl' for val in certkey_values):
        return

    # Check that all files are inside the same directory
    if len(set([os.path.dirname(certkey) for certkey in certkey_values])) == 1:
        path = os.path.dirname(certkey_values[0])
        op.write(CUPSFILES_CONF, 'a',
                 '\n# added by Leapp\nServerKeychain {}\n'.format(path))
    else:
        for value in certkey_values:
            if not os.path.dirname(value) == '/etc/cups/ssl':
                op.copy_to_ssl(value)


def _get_facts(model):
    """
    Consumes input data model

    :param class model: name of model which we consume
    """
    return next(api.consume(model), None)


def migrate_configuration(error_log=api.current_logger().error,
                          debug_log=api.current_logger().debug,
                          op=FileOperations(),
                          consume_function=_get_facts):
    """
    Migrate CUPS configuration based on gathered facts

    :param func error_log: sends error messages
    :param func debug_log: sends debug messages
    :param obj op: IO operations
    :param func consume_function: receives data object from a model
    """

    facts = consume_function(CupsChangedFeatures)
    error_list = []

    if not facts:
        return

    if facts.include:
        debug_log('Migrating CUPS configuration - Include directives.')
        include_errors = []

        include_errors = migrate_include(facts.include_files, op)
        if include_errors:
            error_list += include_errors

    if facts.digest:
        debug_log('Migrating CUPS configuration - BasicDigest/Digest directives.')

        try:
            migrate_digest(op)
        except IOError as error:
            error_list.append('Digest/BasicDigest values: {}'.format(error))

    if facts.env:
        debug_log('Migrating CUPS configuration - PassEnv/SetEnv directives.')

        try:
            move_directives(['PassEnv', 'SetEnv'],
                            op)
        except IOError as error:
            error_list.append('PassEnv/SetEnv directives: {}'.format(error))

    if facts.certkey:
        debug_log('Migrating CUPS configuration - '
                  'ServerKey/ServerCertificate directive.')

        try:
            migrate_certkey(op)
        except IOError as error:
            error_list.append('ServerKey/ServerCertificate directives: {}'.format(error))

    if facts.printcap:
        debug_log('Migrating CUPS configuration - PrintcapFormat directive.')

        try:
            move_directives(['PrintcapFormat'],
                            op)
        except IOError as error:
            error_list.append('PrintcapFormat directive: {}'.format(error))

    if error_list:
        error_log('Following errors happened during CUPS migration:'
                  + ''.join(['\n   - {}'.format(err) for err in error_list]))


# repos/system_upgrade/el7toel8/actors/cupsmigrate/tests/test_migrate_certkey.py

import pytest

from leapp.libraries.actor.cupsmigrate import migrate_certkey


class MockFileSystem(object):
    def __init__(self,
                 infiles):
        self.files = {}
        self.ssl_dir = []
        self.files = infiles
        for path in infiles.keys():
            if path.startswith('/etc/cups/ssl'):
                self.ssl_dir.append(path)

    def readlines(self, path):
        if path in self.files.keys():
            return self.files[path].splitlines(True)
        raise IOError('Error when reading file {} - file '
                      'does not exist.'.format(path))

    def write(self, path, mode, content):
        if isinstance(content, list):
            content = ''.join(content)

        if mode == 'w':
            self.files[path] = content
        else:
            self.files[path] += content

    def copy_to_ssl(self, oldpath):
        self.ssl_dir.append('/etc/cups/ssl/' + oldpath.rsplit('/', 1)[1])


testdata = (
    (
        {
            '/etc/cups/cups-files.conf': 'ifdfdfgfg\n'
        },
        {
            '/etc/cups/cups-files.conf': 'ifdfdfgfg\n'
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerKey /etc/cups/ssl/ser'
                                         'ver.key\nHello world\n',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.key']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': '#ServerKey /etc/cups/ssl/se'
                                         'rver.key\nHello world\n',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': '#ServerKey /etc/cups/ssl/se'
                                         'rver.key\nHello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.key']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate /etc/cups'
                                         '/ssl/server.cert\nHello world\n',
            '/etc/cups/ssl/server.cert': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.cert']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate /etc/cups'
                                         '/ssl/server.cert\nServerKey'
                                         ' /etc/cups/ssl/server.key\n'
                                         'Hello world\n',
            '/etc/cups/ssl/server.cert': '',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.cert',
                        '/etc/cups/ssl/server.key']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate ssl/serve'
                                         'r.cert\nServerKey ssl/serve'
                                         'r.key\nHello world\n',
            '/etc/cups/ssl/server.cert': '',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.cert',
                        '/etc/cups/ssl/server.key']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate ssl/serve'
                                         'r.cert\nServerKey /etc/cups'
                                         '/ssl/server.key\nHello worl'
                                         'd\n',
            '/etc/cups/ssl/server.cert': '',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.cert',
                        '/etc/cups/ssl/server.key']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate ssl/serve'
                                         'r.cert\nServerKey /somewher'
                                         'e/else/server.key\nHello wo'
                                         'rld\n',
            '/etc/cups/ssl/server.cert': '',
            '/somewhere/else/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.cert',
                        '/etc/cups/ssl/server.key']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate /somewher'
                                         'e/else/server.cert\nServerK'
                                         'ey /etc/cups/ssl/server.key'
                                         '\nHello world\n',
            '/somewhere/else/server.cert': '',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.key',
                        '/etc/cups/ssl/server.cert']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate /somewher'
                                         'e/else/server.cert\nServerK'
                                         'ey /somewhere/else/server.c'
                                         'ert\nHello world\n',
            '/somewhere/else/server.cert': '',
            '/somewhere/else/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n\n# added by L'
                                         'eapp\nServerKeychain /somew'
                                         'here/else\n'
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate /somewher'
                                         'e/else/server.cert\nServerK'
                                         'ey /anywhere/else/server.ke'
                                         'y\nHello world\n',
            '/somewhere/else/server.cert': '',
            '/anywhere/else/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/server.cert',
                        '/etc/cups/ssl/server.key']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate ssl/somedir/'
                                         'server.cert\nHello world\nServ'
                                         'erKey ssl/server.key\n',
            '/etc/cups/ssl/somedir/server.cert': '',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/somedir/server.cert',
                        '/etc/cups/ssl/server.key',
                        '/etc/cups/ssl/server.cert']
        }
    ),
    (
        {
            '/etc/cups/cups-files.conf': 'ServerCertificate /etc/cups/ss'
                                         'l/somedir/server.cert\nHello w'
                                         'orld\nServerKey ssl/server.key\n',
            '/etc/cups/ssl/somedir/server.cert': '',
            '/etc/cups/ssl/server.key': ''
        },
        {
            '/etc/cups/cups-files.conf': 'Hello world\n',
            'ssl-dir': ['/etc/cups/ssl/somedir/server.cert',
                        '/etc/cups/ssl/server.key',
                        '/etc/cups/ssl/server.cert']
        }
    )
)


@pytest.mark.parametrize('files,expected', testdata)
def test_migrate_certkey(files, expected):
    op = MockFileSystem(infiles=files)

    migrate_certkey(op)

    assert op.files.get('/etc/cups/cups-files.conf', None) == expected.get('/etc/cups/cups-files.conf', None)
    assert op.ssl_dir == expected.get('ssl-dir', [])


# repos/system_upgrade/el7toel8/actors/cupsmigrate/tests/test_migrate_configuration.py

import pytest

from leapp.libraries.actor.cupsmigrate import migrate_configuration
from leapp.models import CupsChangedFeatures


class MockFileSystem(object):
    def __init__(self,
                 infiles):
        self.files = {}
        self.ssl_dir = []
        self.files = infiles
        for path in infiles.keys():
            if path.startswith('ssl') or path.startswith('/etc/cups/ssl'):
                self.ssl_dir.append(path.rsplit('/', 1)[1])

    def readlines(self, path):
        if path in self.files.keys():
            return self.files[path].splitlines(True)
        raise IOError('Error when reading file {} - file '
                      'does not exist.'.format(path))

    def write(self, path, mode, content):
        if isinstance(content, list):
            content = ''.join(content)

        if mode == 'w':
            self.files[path] = content
        else:
            self.files[path] += content

    def copy_to_ssl(self, oldpath):
        self.ssl_dir.append(oldpath.rsplit('/', 1)[1])


class MockLogger(object):
    def __init__(self):
        self.debug_msg = ''
        self.error_msg = ''

    def debug_log(self, msg):
        self.debug_msg += msg

    def error_log(self, msg):
        self.error_msg += msg


class MockModel(object):
    def __init__(self, facts):
        if not facts:
            self.model = None
            return

        self.model = CupsChangedFeatures(include=facts.get('include', False),
                                         digest=facts.get('digest', False),
                                         env=facts.get('env', False),
                                         certkey=facts.get('certkey', False),
                                         printcap=facts.get('printcap', False),
                                         include_files=facts.get('include_files', []))

    def get_facts(self, model):
        return self.model


testdata = (
    (
        None,
        {
            'debug_msg': '',
            'error_msg': ''
        }
    ),
    (
        {},
        {
            'debug_msg': '',
            'error_msg': ''
        }
    ),
    (
        {
            'include': True,
            'include_files': ['/etc/cups/cupsd.conf', 'smth.conf',
                              'any.conf'],
        },
        {
            'debug_msg': 'Migrating CUPS configuration - Include directives.',
            'error_msg': 'Following errors happened during CUPS migration:\n   '
                         '- Include directive: Error when reading file /etc/cup'
                         's/cupsd.conf - file does not exist.\n   - Include dir'
                         'ective: Error when reading file smth.conf - file does'
                         ' not exist.\n   - Include directive: Error when readi'
                         'ng file any.conf - file does not exist.'
        }
    ),
    (
        {
            'digest': True,
        },
        {
            'debug_msg': 'Migrating CUPS configuration - BasicDigest/Digest'
                         ' directives.',
            'error_msg': 'Following errors happened during CUPS migration:\n   '
                         '- Digest/BasicDigest values: Error when reading file '
                         '/etc/cups/cupsd.conf - file does not exist.'
        }
    ),
    (
        {
            'env': True,
        },
        {
            'debug_msg': 'Migrating CUPS configuration - PassEnv/SetEnv directives.',
            'error_msg': 'Following errors happened during CUPS migration:\n   '
                         '- PassEnv/SetEnv directives: Error when reading file '
                         '/etc/cups/cupsd.conf - file does not exist.'
        }
    ),
    (
        {
            'certkey': True,
        },
        {
            'debug_msg': 'Migrating CUPS configuration - ServerKey/ServerCertif'
                         'icate directive.',
            'error_msg': 'Following errors happened during CUPS migration:\n   '
                         '- ServerKey/ServerCertificate directives: Error when '
                         'reading file /etc/cups/cups-files.conf - file does no'
                         't exist.'
        }
    ),
    (
        {
            'printcap': True,
        },
        {
            'debug_msg': 'Migrating CUPS configuration - PrintcapFormat directive.',
            'error_msg': 'Following errors happened during CUPS migration:\n   '
                         '- PrintcapFormat directive: Error when reading file /'
                         'etc/cups/cupsd.conf - file does not exist.'
        }
    ),
    (
        {
            'certkey': True,
            'include': True,
            'env': True,
            'printcap': True,
            'digest': True,
            'include_files': ['/etc/cups/cupsd.conf', 'smth.conf',
                              'any.conf']
        },
        {
            'debug_msg': 'Migrating CUPS configuration - Include directives.Mig'
                         'rating CUPS configuration - BasicDigest/Digest direct'
                         'ives.Migrating CUPS configuration - PassEnv/SetEnv di'
                         'rectives.Migrating CUPS configuration - ServerKey/Ser'
                         'verCertificate directive.Migrating CUPS configuration'
                         ' - PrintcapFormat directive.',
            'error_msg': 'Following errors happened during CUPS migration:\n   '
                         '- Include directive: Error when reading file /etc/cup'
                         's/cupsd.conf - file does not exist.\n   - Include dir'
                         'ective: Error when reading file smth.conf - file does'
                         ' not exist.\n   - Include directive: Error when readi'
                         'ng file any.conf - file does not exist.\n   - Digest/'
                         'BasicDigest values: Error when reading file /etc/cups'
                         '/cupsd.conf - file does not exist.\n   - PassEnv/SetE'
                         'nv directives: Error when reading file /etc/cups/cups'
                         'd.conf - file does not exist.\n   - ServerKey/ServerC'
                         'ertificate directives: Error when reading file /etc/c'
                         'ups/cups-files.conf - file does not exist.\n   - Prin'
                         'tcapFormat directive: Error when reading file /etc/cu'
                         'ps/cupsd.conf - file does not exist.'
        }
    )
)


@pytest.mark.parametrize('facts,expected', testdata)
def test_migrate_configuration(facts, expected):
    data_model = MockModel(facts)

    op = MockFileSystem({})

    logger = MockLogger()

    migrate_configuration(logger.error_log,
                          logger.debug_log,
                          op,
                          data_model.get_facts)

    assert logger.debug_msg == expected['debug_msg']
    assert logger.error_msg == expected['error_msg']


# repos/system_upgrade/el7toel8/actors/cupsmigrate/tests/test_migrate_digest.py

import pytest

from leapp.libraries.actor.cupsmigrate import migrate_digest


class MockFileSystem(object):
    def __init__(self,
                 infiles):
        self.files = {}
        self.ssl_dir = []
        self.files = infiles
        for path in infiles.keys():
            if path.startswith('ssl') or path.startswith('/etc/cups/ssl'):
                self.ssl_dir.append(path.rsplit('/', 1)[1])

    def readlines(self, path):
        if path in self.files.keys():
            return self.files[path].splitlines(True)
        raise IOError('Error when reading file {} - file '
                      'does not exist.'.format(path))

    def write(self, path, mode, content):
        if isinstance(content, list):
            content = ''.join(content)

        if mode == 'w':
            self.files[path] = content
        else:
            self.files[path] += content

    def copy_to_ssl(self, oldpath):
        self.ssl_dir.append(oldpath.rsplit('/', 1)[1])


testdata = (
    (
        {
            '/etc/cups/cupsd.conf': 'ifdfdfgfg\n'
        },
        'ifdfdfgfg\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'AuthType Basic\nHello world\n',
        },
        'AuthType Basic\nHello world\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'DefaultAuthType Negotiate\nHello world\n',
        },
        'DefaultAuthType Negotiate\nHello world\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'AuthType Digest\nHello world\n',
        },
        'AuthType Basic\nHello world\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'DefaultAuthType Digest\nHello world\n',
        },
        'DefaultAuthType Basic\nHello world\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'DefaultAuthType BasicDigest\nHello world\n',
        },
        'DefaultAuthType Basic\nHello world\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'AuthType BasicDigest\nHello world\n',
        },
        'AuthType Basic\nHello world\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': '#AuthType BasicDigest\nHello world\n',
        },
        '#AuthType BasicDigest\nHello world\n'
    )
)


@pytest.mark.parametrize('files,expected', testdata)
def test_migrate_digest(files, expected):
    op = MockFileSystem(infiles=files)

    migrate_digest(op)

    assert op.files.get('/etc/cups/cupsd.conf', None) == expected


# repos/system_upgrade/el7toel8/actors/cupsmigrate/tests/test_migrate_include.py

import pytest

from leapp.libraries.actor.cupsmigrate import migrate_include


class MockFileSystem(object):
    def __init__(self,
                 infiles):
        self.files = {}
        self.ssl_dir = []
        self.files = infiles
        for path in infiles.keys():
            if path.startswith('ssl') or path.startswith('/etc/cups/ssl'):
                self.ssl_dir.append(path.rsplit('/', 1)[1])

    def readlines(self, path):
        if path in self.files.keys():
            return self.files[path].splitlines(True)
        raise IOError('Error when reading file {} - file '
                      'does not exist.'.format(path))

    def write(self, path, mode, content):
        if isinstance(content, list):
            content = ''.join(content)

        if mode == 'w':
            self.files[path] = content
        else:
            self.files[path] += content

    def copy_to_ssl(self, oldpath):
        self.ssl_dir.append(oldpath.rsplit('/', 1)[1])


testdata = (
    (
        {
            '/etc/cups/cupsd.conf': 'ifdfdfgfg\n'
        },
        ['/etc/cups/cupsd.conf'],
        'ifdfdfgfg\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include smth.conf\nHello world\n',
            'smth.conf': 'Policy two\n'
        },
        ['/etc/cups/cupsd.conf', 'smth.conf'],
        'Hello world\n\n# added by Leapp\nPolicy two\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include smth.conf\nHello world\n',
            'smth.conf': 'Include any.conf\nMake my day\n',
            'any.conf': 'Go ahead\n'
        },
        ['/etc/cups/cupsd.conf', 'smth.conf', 'any.conf'],
        'Hello world\n\n# added by Leapp\nMake my day\n\n# added by Leapp\nGo ahead\n'
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include smth.conf\nHello world\n',
            'smth.conf': '#Include any.conf\nMake my day\n',
            'any.conf': 'Go ahead\n'
        },
        ['/etc/cups/cupsd.conf', 'smth.conf'],
        'Hello world\n\n# added by Leapp\n#Include any.conf\nMake my day\n'
    )
)


@pytest.mark.parametrize('files,included_files,expected', testdata)
def test_migrate_include(files, included_files, expected):
    op = MockFileSystem(infiles=files)

    migrate_include(included_files, op)

    assert op.files.get('/etc/cups/cupsd.conf', None) == expected


# repos/system_upgrade/el7toel8/actors/cupsmigrate/tests/test_move_directives.py

import pytest

from leapp.libraries.actor.cupsmigrate import move_directives


class MockFileSystem(object):
    def __init__(self,
                 infiles):
        self.files = {}
        self.ssl_dir = []
        self.files = infiles
        for path in infiles.keys():
            if path.startswith('ssl') or path.startswith('/etc/cups/ssl'):
                self.ssl_dir.append(path.rsplit('/', 1)[1])

    def readlines(self, path):
        if path in self.files.keys():
            return self.files[path].splitlines(True)
        raise IOError('Error when reading file {} - file '
                      'does not exist.'.format(path))

    def write(self, path, mode, content):
        if isinstance(content, list):
            content = ''.join(content)

        if mode == 'w':
            self.files[path] = content
        else:
            self.files[path] += content

    def copy_to_ssl(self, oldpath):
        self.ssl_dir.append(oldpath.rsplit('/', 1)[1])


testdata = (
    (
        {
            '/etc/cups/cupsd.conf': 'ifdfdfgfg\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        },
        {
            '/etc/cups/cupsd.conf': 'ifdfdfgfg\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': '#PassEnv smht\nHello world\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        },
        {
            '/etc/cups/cupsd.conf': '#PassEnv smht\nHello world\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'PassEnv smth\nHello world\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        },
        {
            '/etc/cups/cupsd.conf': 'Hello world\n',
            '/etc/cups/cups-files.conf': 'clean\n\n# added by Leapp\nPassEnv smth\n'
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'SetEnv smht to\nHello world\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        },
        {
            '/etc/cups/cupsd.conf': 'Hello world\n',
            '/etc/cups/cups-files.conf': 'clean\n\n# added by Leapp\nSetEnv smht to\n'
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'PassEnv smht\nSetEnv set to\nHello world\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        },
        {
            '/etc/cups/cupsd.conf': 'Hello world\n',
            '/etc/cups/cups-files.conf': 'clean\n\n# added by Leapp\n'
                                         'PassEnv smht\nSetEnv set to\n'
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'PassEnv smth\nSetEnv set to\nPri'
                                    'ntcapFormat any\nHello world\n',
            '/etc/cups/cups-files.conf': 'clean\n'
        },
        {
            '/etc/cups/cupsd.conf': 'Hello world\n',
            '/etc/cups/cups-files.conf': 'clean\n\n# added by Leapp\n'
                                         'PassEnv smth\nSetEnv set to'
                                         '\nPrintcapFormat any\n'
        }
    )
)


@pytest.mark.parametrize('files,expected', testdata)
def test_move_directives(files, expected):
    op = MockFileSystem(infiles=files)

    move_directives(['PassEnv', 'SetEnv', 'PrintcapFormat'], op)

    assert op.files.get('/etc/cups/cupsd.conf', None) == expected.get('/etc/cups/cupsd.conf', None)
    assert op.files.get('/etc/cups/cups-files.conf', None) == expected.get('/etc/cups/cups-files.conf', None)


# repos/system_upgrade/el7toel8/actors/cupsscanner/libraries/cupsscanner.py

import os

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import CupsChangedFeatures, InstalledRedHatSignedRPM


def _list_dir(path):
    """
    Lists files which are in a directory specified by the path.

    :param str path: path to directory
    """
    return os.listdir(path)


def _path_exists(path):
    """
    Checks if the path exists on the machine.

    :param str path: path to file/directory
    """
    return os.path.exists(path)


def _read_file(path):
    """
    Read a file line by line.

    :param str path: path to file
    """
    with open(path, 'r') as f:
        return f.readlines()


def _check_package(pkg):
    """
    Checks if a package is installed and signed.

    :param str pkg: name of package
    """
    return has_package(InstalledRedHatSignedRPM, pkg)


def directive_exists(name, line):
    """
    Checks if directive exists in the line, but it is not
    commented out.

    :param str name: name of directive
    :param str line: line of file
    """
    return line.lstrip().startswith(name)


def get_directive_value(name, line):
    """
    Returns directive value.

    :param str name: name of directive
    :param str line: line of file
    """
    if directive_exists(name, line):
        return line.lstrip().lstrip(name).lstrip().split(' ')[0].rstrip()
    return None


def interface_script_check(check_path_func=_path_exists,
                           list_dir_func=_list_dir):
    """
    Checks if any file is in /etc/cups/interfaces, which means there could be
    print queues using interface script.

    :param func check_path_func: checks if /etc/cups/interfaces exists
    :param func list_dir_func: lists contents of directory
    """
    if (
            check_path_func('/etc/cups/interfaces') and
            list_dir_func('/etc/cups/interfaces')
       ):
        return True
    return False


def include_directive_check(read_func=_read_file):
    """
    Checks if 'Include' directive is present.

    :param str paths: path to cupsd configuration file
    :param func read_func: function for reading a file as lines
    """
    included_files = ['/etc/cups/cupsd.conf']
    error_list = []

    for included_file in included_files:
        try:
            lines = read_func(included_file)
        except IOError:
            error_list.append('Error during reading file {}: file not'
                              ' found'.format(included_file))
            included_files.remove(included_file)
            continue

        for line in lines:
            value = get_directive_value('Include', line)
            if value:
                included_files.append(value)

    return (included_files, error_list)


def digest_directive_check(path, read_func=_read_file):
    """
    Checks if AuthType or DefaultAuthType directives contain
    Digest or BasicDigest values, which were removed.

    :param str path: path to configuration file
    :param func read_func: function for reading the file
    """
    lines = read_func(path)

    for line in lines:
        for name in ['AuthType', 'DefaultAuthType']:
            for value in ['Digest', 'BasicDigest']:
                found_value = get_directive_value(name, line)
                if found_value == value:
                    return True
    return False


def ssl_directive_check(read_func=_read_file):
    """
    Checks if ServerCertificate or ServerKey directives are
    used in cups-files.conf.

    :param func read_func: function for reading the file
    """
    lines = read_func('/etc/cups/cups-files.conf')

    for line in lines:
        for name in ['ServerCertificate', 'ServerKey']:
            value = get_directive_value(name, line)
            if value:
                return True
    return False


def environment_setup_check(path, read_func=_read_file):
    """
    Checks if PassEnv or SetEnv directives are used in configuration.
    They were moved to cups-files.conf in newer CUPS due security
    issues.

    :param str path: path to configuration file
    :param func read_func: reads the file
    """
    lines = read_func(path)

    for line in lines:
        for name in ['SetEnv', 'PassEnv']:
            value = get_directive_value(name, line)
            if value:
                return True
    return False


def print_capabilities_check(path, read_func=_read_file):
    """
    Checks if PrintcapFormat directive is used in configuration.
    It was moved to cups-files.conf in newer CUPS.

    :param str path: path to configuration file
    :param func read_func: reads the file
    """
    lines = read_func(path)

    for line in lines:
        value = get_directive_value('PrintcapFormat', line)
        if value:
            return True
    return False


def _send_model(interface, digest, include, certkey, env,
                printcap, include_files_list):
    """
    Produces model of facts.

    :param bool interface: true if interface scripts are used
    :param bool digest: true if BasicDigest/Digest values are used
    :param bool include: true if Include directive is used
    :param bool certkey: true if ServerCertificate/ServerKey directives are used
    :param bool env: true if PassEnv/SetEnv directives are used
    :param bool printcap: true if PrintcapFormat directive is used
    :param list include_files_list: contains paths to included files
    """
    api.produce(CupsChangedFeatures(interface=interface,
                                    digest=digest,
                                    include=include,
                                    certkey=certkey,
                                    env=env,
                                    printcap=printcap,
                                    include_files=include_files_list))


def find_features(debug_log=api.current_logger().debug,
                  warn_log=api.current_logger().warn,
                  error_log=api.current_logger().error,
                  send_features=_send_model,
                  is_installed=_check_package,
                  read_func=_read_file,
                  check_path_func=_path_exists,
                  list_dir_func=_list_dir):
    """
    Checks every feature which changed between CUPS 1.6.3 and CUPS
    2.2.6.

    :param func debug_log: function for debug logging
    :param func error_log: function for error logging
    :param func warn_log: function for warning logging
    :param func send_features: produces CupsMigrationModel if necessary
    :param func is_installed: check if the package is installed
    :param func read_func: reads a file
    :param func check_path_func: checks if the file exists
    :param func list_dir_func: list files in a directory
    """

    if not is_installed('cups'):
        return

    if (
        not check_path_func('/etc/cups/cupsd.conf') or
        not check_path_func('/etc/cups/cups-files.conf')
    ):
        # seatbelt - it's expected as super rare to have malfunction cupsd :)
        raise StopActorExecutionError('Core CUPS configuration files '
                                      'are missing. CUPS installation '
                                      'is corrupted, terminating the actor.')

    debug_log('Checking if CUPS configuration contains removed features.')

    digest = env = printcap = interface = certkey = include = False

    include_file_list, error_list = include_directive_check(read_func)

    if error_list:
        warn_log('Following included files will not be appended to '
                 'cupsd.conf due attached error:'
                 + ''.join(['\n   - {}'.format(err) for err in error_list]))

    if len(include_file_list) > 1:
        include = True

    interface = interface_script_check(check_path_func, list_dir_func)

    for config_file in include_file_list:

        if not digest:
            digest = digest_directive_check(config_file, read_func)

        if not env:
            env = environment_setup_check(config_file, read_func)

        if not printcap:
            printcap = print_capabilities_check(config_file, read_func)

    certkey = ssl_directive_check(read_func)

    if any([interface, digest, include, certkey, env, printcap]):
        send_features(interface, digest, include, certkey, env,
                      printcap, include_file_list)


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_digest_directive_check.py

import pytest

from leapp.libraries.actor.cupsscanner import digest_directive_check

testdata = (
    ('\n', False),
    ('test\n', False),
    ('AuthType Basic\n', False),
    ('DefaultAuthType Basic\n', False),
    ('#AuthType Digest\n', False),
    ('#DefaultAuthType BasicDigest\n', False),
    ('DefaultAuthType BasicDigest\n', True),
    ('DefaultAuthType Digest\n', True),
    ('AuthType Digest\n', True),
    ('AuthType BasicDigest\n', True),
    ('AuthType BasicDigest\nDefaultAuthType Digest\n', True),
)


class MockConfig(object):
    def __init__(self, content):
        self.content = content

    def read(self, path):
        return self.content.splitlines(True)


@pytest.mark.parametrize("content,expected", testdata)
def test_digest_directive_check(content, expected):
    config = MockConfig(content)

    ret = digest_directive_check('does_not_matter', config.read)

    assert ret == expected


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_directive_exists.py

import pytest

from leapp.libraries.actor.cupsscanner import directive_exists

testdata = (
    ('PrintcapFormat', 'ble', False),
    ('PrintcapFormat', '', False),
    ('PrintcapFormat', '#PrintcapFormat', False),
    ('PrintcapFormat', 'PrintcapFormat', True),
    ('PrintcapFormat', '    PrintcapFormat', True)
)


@pytest.mark.parametrize("string, line, expected", testdata)
def test_directive_exists(string, line, expected):

    ret = directive_exists(string, line)

    assert ret == expected


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_environment_setup_check.py

import pytest

from leapp.libraries.actor.cupsscanner import environment_setup_check

testdata = (
    ('\n', False),
    ('Something else\n', False),
    ('#PassEnv smth\n', False),
    ('   #SetEnv smth\n', False),
    ('PassEnv smth\n', True),
    ('SetEnv smth\n', True),
    ('PassEnv\n', False),
    ('SetEnv\n', False),
    ('PassEnv smth\nSetEnv smth\n', True)
)


class MockCUPSD(object):
    def __init__(self, content):
        self.content = content

    def read(self, path):
        if path:
            return self.content.splitlines(True)
        return None


@pytest.mark.parametrize("content, expected", testdata)
def test_environment_setup_check(content, expected):
    config = MockCUPSD(content)

    ret = environment_setup_check('does_not_matter', config.read)

    assert ret == expected


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_find_features.py

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor.cupsscanner import find_features

message = 'Checking if CUPS configuration contains removed features.'

testdata = (
    (
        ['ble'],
        {},
        {}
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': '',
            '/etc/cups/cups-files.conf': ''
        },
        {
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'Include smth\n',
            '/etc/cups/cups-files.conf': '',
            'smth': ''
        },
        {
            'include': True,
            'digest': False,
            'interface': False,
            'env': False,
            'certkey': False,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf', 'smth'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'Include smth\n',
            '/etc/cups/cups-files.conf': '',
        },
        {
            'debug': message,
            'warn': 'Following included files will not be appended to cupsd.c'
            'onf due attached error:\n   - Error during reading file smth: file not found'
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'Include smth\nInclude smb\n',
            '/etc/cups/cups-files.conf': '',
            'smth': '',
            'smb': ''
        },
        {
            'include': True,
            'digest': False,
            'interface': False,
            'env': False,
            'certkey': False,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf', 'smth', 'smb'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'Include smth\n',
            '/etc/cups/cups-files.conf': '',
            'smth': 'AuthType Digest\nPassEnv smth\nPrintcapFormat neco\n'
        },
        {
            'include': True,
            'digest': True,
            'interface': False,
            'env': True,
            'certkey': False,
            'printcap': True,
            'included_files': ['/etc/cups/cupsd.conf', 'smth'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': '',
            '/etc/cups/cups-files.conf': 'ServerKey smth.key\n',
            'smth.key': ''
        },
        {
            'include': False,
            'digest': False,
            'interface': False,
            'env': False,
            'certkey': True,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': '',
            '/etc/cups/cups-files.conf': 'ServerCertificate smth.cert\n',
            'smth.cert': ''
        },
        {
            'include': False,
            'digest': False,
            'interface': False,
            'env': False,
            'certkey': True,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': '',
            '/etc/cups/cups-files.conf': 'ServerKey smth.key\n'
                                         'ServerCertificate smth.cert\n',
            'smth.key': '',
            'smth.cert': ''
        },
        {
            'include': False,
            'digest': False,
            'interface': False,
            'env': False,
            'certkey': True,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'AuthType Digest\n',
            '/etc/cups/cups-files.conf': '',
        },
        {
            'include': False,
            'digest': True,
            'interface': False,
            'env': False,
            'certkey': False,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'DefaultAuthType BasicDigest\n',
            '/etc/cups/cups-files.conf': '',
        },
        {
            'include': False,
            'digest': True,
            'interface': False,
            'env': False,
            'certkey': False,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'PassEnv smth\n',
            '/etc/cups/cups-files.conf': '',
        },
        {
            'include': False,
            'digest': False,
            'interface': False,
            'env': True,
            'certkey': False,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'SetEnv smth\n',
            '/etc/cups/cups-files.conf': '',
        },
        {
            'include': False,
            'digest': False,
            'interface': False,
            'env': True,
            'certkey': False,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'PrintcapFormat smth\n',
            '/etc/cups/cups-files.conf': '',
        },
        {
            'include': False,
            'digest': False,
            'interface': False,
            'env': False,
            'certkey': False,
            'printcap': True,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': '',
            '/etc/cups/cups-files.conf': '',
            '/etc/cups/interfaces': []
        },
        {
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': '',
            '/etc/cups/cups-files.conf': '',
            '/etc/cups/interfaces': ['smth', 'anything'],
            'smth': '',
            'anything': ''
        },
        {
            'include': False,
            'digest': False,
            'interface': True,
            'env': False,
            'certkey': False,
            'printcap': False,
            'included_files': ['/etc/cups/cupsd.conf'],
            'debug': message
        }
    ),
    (
        ['cups'],
        {
            '/etc/cups/cupsd.conf': 'Include mst\nAuthType Digest\n'
                                    'PassEnv too\nPrintcapFormat poo\n',
            '/etc/cups/cups-files.conf': 'ServerKey my.key\n'
                                         'ServerCertificate my.cert\n',
            '/etc/cups/interfaces': ['smth', 'anything'],
            'smth': '',
            'anything': '',
            'mst': ''
        },
        {
            'include': True,
            'digest': True,
            'interface': True,
            'env': True,
            'certkey': True,
            'printcap': True,
            'included_files': ['/etc/cups/cupsd.conf', 'mst'],
            'debug': message
        }
    )
)


class MockActor(object):
    def __init__(self):
        self.output = {}

    def send_features(self, interface, digest, include, certkey, env,
                      printcap, included_files):
        self.output['interface'] = interface
        self.output['digest'] = digest
        self.output['include'] = include
        self.output['certkey'] = certkey
        self.output['env'] = env
        self.output['printcap'] = printcap
        self.output['included_files'] = included_files


class MockLogger(object):
    def __init__(self):
        self.debugmsg = ''
        self.warnmsg = ''
        self.errormsg = ''

    def debug(self, message):
        self.debugmsg += message

    def error(self, message):
        self.errormsg += message

    def warn(self, message):
        self.warnmsg += message


class MockFileSystem(object):
    def __init__(self, packages, files):
        self.installed_packages = packages
        self.files = files

    def is_installed(self, pkg):
        if pkg in self.installed_packages:
            return True
        return False

    def read(self, path):
        if path in self.files.keys():
            return self.files[path].splitlines(True)
        raise IOError('Error during reading file {} - file'
                      ' not found.'.format(path))

    def path_exists(self, path):
        if path in self.files.keys():
            return True
        return False

    def list_dir(self, path):
        if path in self.files.keys():
            return self.files[path]
        return False


def test_find_features_exception():
    logger = MockLogger()
    system = MockFileSystem(['cups'], {})
    actor = MockActor()

    with pytest.raises(StopActorExecutionError):
        find_features(logger.debug,
                      logger.warn,
                      logger.error,
                      actor.send_features,
                      system.is_installed,
                      system.read,
                      system.path_exists,
                      system.list_dir)


@pytest.mark.parametrize(("packages,files,expected"), testdata)
def test_find_features(packages, files, expected):
    logger = MockLogger()
    system = MockFileSystem(packages, files)
    actor = MockActor()

    find_features(logger.debug,
                  logger.warn,
                  logger.error,
                  actor.send_features,
                  system.is_installed,
                  system.read,
                  system.path_exists,
                  system.list_dir)

    assert actor.output.get('interface', None) == expected.get('interface', None)
    assert actor.output.get('digest', None) == expected.get('digest', None)
    assert actor.output.get('include', None) == expected.get('include', None)
    assert actor.output.get('certkey', None) == expected.get('certkey', None)
    assert actor.output.get('env', None) == expected.get('env', None)
    assert actor.output.get('printcap', None) == expected.get('printcap', None)
    assert actor.output.get('included_files', None) == expected.get('included_files', None)
    assert logger.debugmsg == expected.get('debug', '')
    assert logger.warnmsg == expected.get('warn', '')
    assert logger.errormsg == expected.get('error', '')


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_get_directive_value_check.py

import pytest

from leapp.libraries.actor.cupsscanner import get_directive_value

testdata = (
    ('Include', 'Include smth', 'smth'),
    ('Include', 'something_else', None),
    ('Include', 'Include', ''),
    ('Include', '#Include smth', None),
    ('Include', '   Include smth', 'smth'),
    ('Include', '   Include smth anything', 'smth'),
)


@pytest.mark.parametrize('string, line, expected', testdata)
def test_get_directive_value(string, line, expected):

    value = get_directive_value(string, line)

    assert value == expected


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_include_directive_check.py

import pytest

from leapp.libraries.actor.cupsscanner import include_directive_check

testdata = (
    (
        {
            '/etc/cups/cupsd.conf': '\n'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf'],
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include smth.conf\n',
            'smth.conf': '\n'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf', 'smth.conf'],
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include smth.conf\nInclude smb.conf\n',
            'smth.conf': '\n',
            'smb.conf': '\n'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf', 'smth.conf',
                               'smb.conf'],
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include smth.conf\n',
            'smth.conf': 'Include smb.conf\n',
            'smb.conf': '\n'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf', 'smth.conf',
                               'smb.conf'],
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include smth.conf\n',
            'smth.conf': 'Include smb.conf\n',
            'smb.conf': 'Include any.conf\n',
            'any.conf': '\n'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf', 'smth.conf',
                               'smb.conf', 'any.conf'],
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': '#Include smth.conf'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf']
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': 'Include\n'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf'],
        }
    ),
    (
        {
            '/etc/cups/cupsd.conf': '    Include smth.conf smth_more\n',
            'smth.conf': '\n'
        },
        {
            'included_files': ['/etc/cups/cupsd.conf', 'smth.conf']
        }
    )
)


class MockFileSystem(object):
    def __init__(self, infiles):
        self.files = infiles

    def read(self, path):
        if path in self.files.keys():
            return self.files[path].splitlines(True)
        raise IOError('Error during reading file.')


@pytest.mark.parametrize("files,expected", testdata)
def test_include_directive_check(files, expected):
    f = MockFileSystem(files)

    included_files, error_list = include_directive_check(read_func=f.read)

    assert included_files == expected.get('included_files', [])
    assert error_list == expected.get('error_list', [])


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_interface_script_check.py

import pytest

from leapp.libraries.actor.cupsscanner import interface_script_check

testdata = (
    ('bla', [], False),
    ('/etc/cups/interfaces', [], False),
    ('/etc/cups/interfaces', ['smth'], True),
)


class MockFilesystem(object):
    def __init__(self, path, files):
        self.path = path
        self.files = files

    def check_path(self, path):
        if self.path == path:
            return True
        return False

    def list_dir(self, path):
        if self.path == path:
            return self.files
        return []


@pytest.mark.parametrize("path,files,expected", testdata)
def test_interface_script_check(path, files, expected):
    filesystem = MockFilesystem(path, files)

    ret = interface_script_check(filesystem.check_path,
                                 filesystem.list_dir)

    assert ret == expected


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_print_capabilities_check.py

import pytest

from leapp.libraries.actor.cupsscanner import print_capabilities_check

testdata = (
    ('\n', False),
    ('Something else\n', False),
    ('#PrintcapFormat smth\n', False),
    ('PrintcapFormat\n', False),
    ('PrintcapFormat smth\n', True)
)


class MockCUPSD(object):
    def __init__(self, content):
        self.content = content

    def read(self, path):
        if path:
            return self.content.splitlines(True)
        return None


@pytest.mark.parametrize("content, expected", testdata)
def test_print_capabilities_check(content, expected):
    config = MockCUPSD(content)

    ret = print_capabilities_check('does_not_matter', config.read)

    assert ret == expected


# repos/system_upgrade/el7toel8/actors/cupsscanner/tests/test_ssl_directive_check.py

import pytest

from leapp.libraries.actor.cupsscanner import ssl_directive_check

testdata = (
    ('\n', False),
    ('smth\n', False),
    ('#ServerCertificate my.crt\n', False),
    ('#ServerKey my.key\n', False),
    ('ServerCertificate\n', False),
    ('ServerKey\n', False),
    ('ServerKey my.key\n', True),
    ('ServerCertificate my.crt\n', True),
    ('ServerCertificate my.crt\nServerKey my.key\n', True)
)


class MockCupsfiles(object):
    def __init__(self, content):
        self.content = content

    def read(self, path):
        return self.content.splitlines(True)


@pytest.mark.parametrize("content,expected", testdata)
def test_ssl_directive_check(content, expected):
    config = MockCupsfiles(content)

    ret = ssl_directive_check(config.read)

    assert ret == expected


# repos/system_upgrade/el7toel8/actors/detectgrubconfigerror/libraries/scanner.py

import re


def detect_config_error(conf_file):
    """
    Check grub configuration for syntax error in GRUB_CMDLINE_LINUX value.

    :return: Function returns True if error was detected, otherwise False.
    """
    with open(conf_file, 'r') as f:
        config = f.read()

    pattern = r'GRUB_CMDLINE_LINUX="[^"]+"(?!(\s*$)|(\s+(GRUB|#)))'
    return re.search(pattern, config) is not None


# repos/system_upgrade/el7toel8/actors/detectgrubconfigerror/tests/test_detectgrubconfigerror.py

import os

from leapp.libraries.actor.scanner import detect_config_error

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def test_correct_config():
    assert not detect_config_error(os.path.join(CUR_DIR, 'files/grub.correct'))
    assert not detect_config_error(os.path.join(CUR_DIR, 'files/grub.correct_trailing_space'))
    assert not detect_config_error(os.path.join(CUR_DIR, 'files/grub.correct_comment'))
    assert not detect_config_error(os.path.join(CUR_DIR, 'files/grub.correct_puppet'))


def test_wrong_config():
    assert detect_config_error(os.path.join(CUR_DIR, 'files/grub.wrong'))
    assert detect_config_error(os.path.join(CUR_DIR, 'files/grub.wrong1'))


# repos/system_upgrade/el7toel8/actors/firewalldfactsactor/libraries/firewalldfactsactor.py

def getLockdownFirewallConfigCommand(root):
    for command in root.iter('command'):
        if 'name' in command.attrib and \
           '/usr/bin/firewall-config' in command.attrib['name']:
            return command.attrib['name']

    return ''


def getEbtablesTablesInUse(root):
    tables = []
    for rule in root.iter('rule'):
        if 'ipv' in rule.attrib and rule.attrib['ipv'] == 'eb' and \
           'table' in rule.attrib and rule.attrib['table'] not in tables:
            tables.append(rule.attrib['table'])

    for passthrough in root.iter('passthrough'):
        if 'ipv' in passthrough.attrib and passthrough.attrib['ipv'] == 'eb':
            rule = passthrough.text.split()
            try:
                i = rule.index('-t')
                if rule[i + 1] not in tables:
                    tables.append(rule[i + 1])
            except ValueError:
                pass

    return tables


def getIpsetTypesInUse(root):
    types = []
    for ipset in root.iter('ipset'):
        if 'type' in ipset.attrib and ipset.attrib['type'] not in types:
            types.append(ipset.attrib['type'])
    return types


# repos/system_upgrade/el7toel8/actors/firewalldfactsactor/tests/component_test_firewalldfactsactor.py

from leapp.models import FirewalldFacts


def test_actor_execution(current_actor_context):
    current_actor_context.run()
    assert current_actor_context.consume(FirewalldFacts)


# repos/system_upgrade/el7toel8/actors/firewalldfactsactor/tests/unit_test_firewalldfactsactor.py

import xml.etree.ElementTree as ElementTree

from leapp.libraries.actor import firewalldfactsactor


def test_firewalldfactsactor_direct():
    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <direct>
             <passthrough ipv="eb">-t broute -I BROUTING 1 -j ACCEPT</passthrough>
           </direct>
        ''')
    assert firewalldfactsactor.getEbtablesTablesInUse(root) == ['broute']

    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <direct>
             <rule priority="1" table="broute" ipv="eb" chain="BROUTING">-j ACCEPT</rule>
           </direct>
        ''')
    assert firewalldfactsactor.getEbtablesTablesInUse(root) == ['broute']

    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <direct>
             <rule priority="1" table="broute" ipv="eb" chain="BROUTING">-j ACCEPT</rule>
             <rule priority="1" table="filter" ipv="ipv4" chain="INPUT">-j ACCEPT</rule>
             <passthrough ipv="eb">-t nat -I PREROUTING 1 -j ACCEPT</passthrough>
           </direct>
        '''
    )
    assert set(firewalldfactsactor.getEbtablesTablesInUse(root)) == set(['broute', 'nat'])


def test_firewalldfactsactor_firewallConfigCommand():
    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <whitelist>
             <command name="/usr/bin/python -Es /usr/bin/firewall-config"/>
             <command name="/usr/bin/foobar"/>
             <selinux context="system_u:system_r:NetworkManager_t:s0"/>
             <selinux context="system_u:system_r:virtd_t:s0-s0:c0.c1023"/>
             <user id="0"/>
           </whitelist>
        '''
    )
    assert firewalldfactsactor.getLockdownFirewallConfigCommand(root) == '/usr/bin/python -Es /usr/bin/firewall-config'

    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <whitelist>
             <command name="/usr/bin/foobar"/>
           </whitelist>
        ''')
    assert firewalldfactsactor.getLockdownFirewallConfigCommand(root) == ''

    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <whitelist>
             <command name="/usr/libexec/platform-python -s /usr/bin/firewall-config"/>
             <selinux context="system_u:system_r:NetworkManager_t:s0"/>
             <selinux context="system_u:system_r:virtd_t:s0-s0:c0.c1023"/>
             <user id="0"/>
           </whitelist>
        ''')
    EXP_RESULT = '/usr/libexec/platform-python -s /usr/bin/firewall-config'
    assert firewalldfactsactor.getLockdownFirewallConfigCommand(root) == EXP_RESULT


def test_firewalldfactsactor_ipsetTypes():
    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <ipset type="hash:ip">
             <short>My Ipset</short>
             <description>description</description>
             <entry>1.2.3.4</entry>
           </ipset>
        ''')
    assert firewalldfactsactor.getIpsetTypesInUse(root) == ['hash:ip']

    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <ipset type="hash:net,port">
             <short>My Ipset</short>
             <description>description</description>
           </ipset>
        ''')
    assert firewalldfactsactor.getIpsetTypesInUse(root) == ['hash:net,port']


# repos/system_upgrade/el7toel8/actors/firewalldupdatelockdownwhitelist/libraries/firewalldupdatelockdownwhitelist.py

def updateFirewallConfigCommand(root, old_command):
    changed = False

    # Only update the command element that corresponds to firewall-config
    new_command = '/usr/libexec/platform-python -s /usr/bin/firewall-config'
    for command in root.iter('command'):
        if 'name' in command.attrib and \
           old_command == command.attrib['name'] and \
           old_command != new_command:
            command.attrib['name'] = new_command
            changed = True

    return changed


# repos/system_upgrade/el7toel8/actors/firewalldupdatelockdownwhitelist/tests/unit_test_firewalldupdatelockdownwhitelist.py

import xml.etree.ElementTree as ElementTree

from leapp.libraries.actor import firewalldupdatelockdownwhitelist


def test_firewalldupdatelockdownwhitelist_library():
    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <whitelist>
             <command name="/usr/bin/python -Es /usr/bin/firewall-config"/>
             <command name="/usr/bin/foobar"/>
             <selinux context="system_u:system_r:NetworkManager_t:s0"/>
             <selinux context="system_u:system_r:virtd_t:s0-s0:c0.c1023"/>
             <user id="0"/>
           </whitelist>
        ''')

    assert firewalldupdatelockdownwhitelist.updateFirewallConfigCommand(
        root,
        '/usr/bin/python -Es /usr/bin/firewall-config'
    )


def test_firewalldupdatelockdownwhitelist_library_negative():
    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <whitelist>
             <command name="/usr/bin/foobar"/>
           </whitelist>
        ''')

    assert not firewalldupdatelockdownwhitelist.updateFirewallConfigCommand(root, '')
    assert not firewalldupdatelockdownwhitelist.updateFirewallConfigCommand(
        root,
        '/usr/bin/python -Es /usr/bin/firewall-config'
    )

    root = ElementTree.fromstring(
        '''<?xml version="1.0" encoding="utf-8"?>
           <whitelist>
             <command name="/usr/libexec/platform-python -s /usr/bin/firewall-config"/>
             <selinux context="system_u:system_r:NetworkManager_t:s0"/>
             <selinux context="system_u:system_r:virtd_t:s0-s0:c0.c1023"/>
             <user id="0"/>
           </whitelist>
        ''')

    assert not firewalldupdatelockdownwhitelist.updateFirewallConfigCommand(
        root,
        '/usr/libexec/platform-python -s /usr/bin/firewall-config'
    )


# repos/system_upgrade/el7toel8/actors/grubdevname/libraries/grubdevname.py

import os

from leapp.libraries.stdlib import run, api, CalledProcessError
from leapp.exceptions import StopActorExecution
from leapp.models import GrubDevice
from leapp.utils.deprecation import suppress_deprecation


def has_grub(blk_dev):
    """
    Check whether GRUB is present on block device
    """
    try:
        blk = os.open(blk_dev, os.O_RDONLY)
        mbr = os.read(blk, 512)
    except OSError:
        api.current_logger().warning(
            'Could not read first sector of {} in order to identify the bootloader'.format(blk_dev)
        )
        raise StopActorExecution()
    os.close(blk)
    test = 'GRUB'
    if not isinstance(mbr, str):
        test = test.encode('utf-8')

    return test in mbr


def blk_dev_from_partition(partition):
    """
    Find parent device of /boot partition
    """
    try:
        result = run(['lsblk', '-spnlo', 'name', partition])
    except CalledProcessError:
        api.current_logger().warning(
            'Could not get parent device of {} partition'.format(partition)
        )
        raise StopActorExecution()
    # lsblk "-s" option prints dependencies in inverse order, so the parent device will always
    # be the last or the only device.
    # Command result example:
    # 'result', {'signal': 0, 'pid': 3872, 'exit_code': 0, 'stderr': u'', 'stdout': u'/dev/vda1\n/dev/vda\n'}
    return result['stdout'].strip().split()[-1]


def get_boot_partition():
    """
    Get /boot partition
    """
    try:
        # call grub2-probe to identify /boot partition
        result = run(['grub2-probe', '--target=device', '/boot'])
    except CalledProcessError:
        api.current_logger().warning(
            'Could not get name of underlying /boot partition'
        )
        raise StopActorExecution()
    return result['stdout'].strip()


@suppress_deprecation(GrubDevice)
def get_grub_device():
    """
    Get block device where GRUB is located. We assume GRUB is on the same device
    as /boot partition is.

    """
    grub_dev = os.getenv('LEAPP_GRUB_DEVICE', None)
    if grub_dev:
        api.produce(GrubDevice(grub_device=grub_dev))
        return
    boot_partition = get_boot_partition()
    grub_dev = blk_dev_from_partition(boot_partition)
    if grub_dev:
        if has_grub(grub_dev):
            api.produce(GrubDevice(grub_device=grub_dev))


# repos/system_upgrade/el7toel8/actors/grubdevname/tests/test_grubdevname.py

import os

import pytest

from leapp.exceptions import StopActorExecution
from leapp.libraries.actor import grubdevname
from leapp.libraries.common import testutils
from leapp.libraries.stdlib import CalledProcessError, api

BOOT_PARTITION = '/dev/vda1'

BOOT_DEVICE = '/dev/vda'
BOOT_DEVICE_ENV = '/dev/sda'

VALID_DD = b'GRUB GeomHard DiskRead Error'
INVALID_DD = b'Nothing here'

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def raise_call_error(args=None):
    raise CalledProcessError(
        message='A Leapp Command Error occured.',
        command=args,
        result={'signal': None, 'exit_code': 1, 'pid': 0, 'stdout': 'fake', 'stderr': 'fake'}
    )


class RunMocked(object):

    def __init__(self, raise_err=False):
        self.called = 0
        self.args = None
        self.raise_err = raise_err

    def __call__(self, args, encoding=None):
        self.called += 1
        self.args = args
        if self.raise_err:
            raise_call_error(args)

        if self.args == ['grub2-probe', '--target=device', '/boot']:
            stdout = BOOT_PARTITION

        elif self.args == ['lsblk', '-spnlo', 'name', BOOT_PARTITION]:
            stdout = BOOT_DEVICE

        return {'stdout': stdout}


def open_mocked(fn, flags):
    return open(os.path.join(CUR_DIR, 'valid') if fn == BOOT_DEVICE else os.path.join(CUR_DIR, 'invalid'), 'r')


def open_invalid(fn, flags):
    return open(os.path.join(CUR_DIR, 'invalid'), 'r')


def read_mocked(f, size):
    return f.read(size)


def close_mocked(f):
    f.close()


def test_get_grub_device(monkeypatch):
    run_mocked = RunMocked()
    monkeypatch.setattr(grubdevname, 'run', run_mocked)
    monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
    monkeypatch.setattr(os, 'open', open_mocked)
    monkeypatch.setattr(os, 'read', read_mocked)
    monkeypatch.setattr(os, 'close', close_mocked)
    grubdevname.get_grub_device()
    assert grubdevname.run.called == 2
    assert BOOT_DEVICE == api.produce.model_instances[0].grub_device


def test_get_grub_device_fail(monkeypatch):
    run_mocked = RunMocked(raise_err=True)
    monkeypatch.setattr(grubdevname, 'run', run_mocked)
    monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
    monkeypatch.setattr(os, 'open', open_mocked)
    monkeypatch.setattr(os, 'read', read_mocked)
    monkeypatch.setattr(os, 'close', close_mocked)
    with pytest.raises(StopActorExecution):
        grubdevname.get_grub_device()
    assert grubdevname.run.called == 1
    assert not api.produce.model_instances


def test_grub_device_env_var(monkeypatch):
    run_mocked = RunMocked()
    monkeypatch.setenv('LEAPP_GRUB_DEVICE', BOOT_DEVICE_ENV)
    monkeypatch.setattr(grubdevname, 'run', run_mocked)
    monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
    monkeypatch.setattr(os, 'open', open_mocked)
    monkeypatch.setattr(os, 'read', read_mocked)
    monkeypatch.setattr(os, 'close', close_mocked)
    grubdevname.get_grub_device()
    assert grubdevname.run.called == 0
    assert BOOT_DEVICE_ENV == api.produce.model_instances[0].grub_device


def test_device_no_grub(monkeypatch):
    run_mocked = RunMocked()
    monkeypatch.setattr(grubdevname, 'run', run_mocked)
    monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
    monkeypatch.setattr(os, 'open', open_invalid)
    monkeypatch.setattr(os, 'read', read_mocked)
    monkeypatch.setattr(os, 'close', close_mocked)
    grubdevname.get_grub_device()
    assert grubdevname.run.called == 2
    assert not api.produce.model_instances


# repos/system_upgrade/el7toel8/actors/kernel/checkkerneldrivers/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkkerneldrivers import (
    check_drivers,
    get_present_drivers,
    get_removed_drivers,
)
from leapp.models import ActiveKernelModulesFacts
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckKernelDrivers(Actor):
    """
    Actor checks if any loaded RHEL7 kernel driver is missing in the RHEL8.
    If yes, the upgrade process will be inhibited.

    Inhibition is done because missing kernel driver on the RHEL8 system may
    mean that the hardware using such driver would not work on the RHEL8.

    Note:
     - List of kernel drivers missing on the RHEL8 system is located in the
        files/removed_drivers.txt file.
    """
    name = 'check_kernel_drivers'
    consumes = (ActiveKernelModulesFacts,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        removed_file = 'files/removed_drivers.txt'
        conflicting = check_drivers(get_removed_drivers(removed_file), get_present_drivers())

        if conflicting:
            title = ('Detected loaded kernel drivers which have been removed '
                     'in RHEL 8. Upgrade cannot proceed.')
            URL = ('https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/'
                   'considerations_in_adopting_rhel_8/index#removed-device-drivers_hardware-enablement')
            summary = ('Support for the following RHEL 7 '
                       'device drivers has been removed in RHEL 8: \n     - {}'
                       '\nPlease see {} for details.'.format('\n     - '.join(conflicting), URL))
            remediation = ('Please disable detected kernel drivers in '
                           'order to proceed with the upgrade process using the rmmod or modprobe -r.')
            create_report([
                reporting.Title(title),
                reporting.Summary(summary),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags([reporting.Tags.KERNEL, reporting.Tags.DRIVERS]),
                reporting.Flags([reporting.Flags.INHIBITOR]),
                reporting.Remediation(hint=remediation)
            ] + [reporting.RelatedResource('kernel-driver', kd) for kd in conflicting])


# repos/system_upgrade/el7toel8/actors/migratebrltty/libraries/migratebrltty.py

import re
import shutil

BackupSuffix = '.bak'


def migrate_file(fn, migrate_bt, migrate_espeak):
    # nothing to migrate
    if not fn or (not migrate_bt and not migrate_espeak):
        return

    # make backup
    shutil.copy2(fn, fn + BackupSuffix)

    regex_bt = re.compile(r'\b(?:(?:bth)|(?:bluez))((?:[:\-][0-9a-fA-F]{2}){6})\b')
    regex_espeak = re.compile(r'^(\s*speech-driver\s+)es\b')

    with open(fn, 'w') as file_out:
        with open(fn + BackupSuffix) as file_in:
            for line in file_in:
                if migrate_bt and regex_bt.search(line):
                    line = regex_bt.sub(r'bluetooth\1', line)
                elif migrate_espeak and regex_espeak.search(line):
                    line = regex_espeak.sub(r'\1en', line)
                file_out.write(line)


# repos/system_upgrade/el7toel8/actors/migratebrltty/tests/component_test_migratebrltty.py

import pytest
from six import text_type

from leapp.models import BrlttyMigrationDecision


@pytest.mark.parametrize('test_input,expected,migrate_bt,migrate_espeak', [
    ('braille-device bth:AB-cd:ef:01:23:45\n', 'braille-device bluetooth:AB-cd:ef:01:23:45', True, False),
    ('braille-device bluez:AB-cd:ef:01:23:45\n', 'braille-device bluetooth:AB-cd:ef:01:23:45', True, False),
    ('speech-driver es\n', 'speech-driver en', False, True),
    ('braille-device bth:AB-cd:ef:01:23:45\nbraille-device bluez:AB-cd:ef:01:23:45\nspeech-driver es\n',
        'braille-device bluetooth:AB-cd:ef:01:23:45\nbraille-device bluetooth:AB-cd:ef:01:23:45\nspeech-driver es',
        True, False),
    ('braille-device bth:AB-cd:ef:01:23:45\nbraille-device bluez:AB-cd:ef:01:23:45\nspeech-driver es\n',
        'braille-device bth:AB-cd:ef:01:23:45\nbraille-device bluez:AB-cd:ef:01:23:45\nspeech-driver en', False, True),
    ('braille-device bth:AB-cd:ef:01:23:45\nbraille-device bluez:AB-cd:ef:01:23:45\nspeech-driver es\n',
        'braille-device bluetooth:AB-cd:ef:01:23:45\nbraille-device bluetooth:AB-cd:ef:01:23:45\nspeech-driver en',
        True, True),
])
def test_actor_migrate(tmpdir, test_input, expected, migrate_bt, migrate_espeak, current_actor_context):
    brltty_conf = text_type(tmpdir.join('brltty.conf'))
    with open(brltty_conf, 'w') as file_out:
        file_out.write(test_input)
    current_actor_context.feed(BrlttyMigrationDecision(migrate_file=brltty_conf, migrate_bt=migrate_bt,
                               migrate_espeak=migrate_espeak))
    current_actor_context.run()
    with open(brltty_conf, 'r') as file_in:
        data = file_in.read().strip()
    assert expected == data


# repos/system_upgrade/el7toel8/actors/migratentp/libraries/migratentp.py

import base64
import io
import tarfile

from leapp.exceptions import StopActorExecutionError
from leapp import reporting
from leapp.libraries.stdlib import CalledProcessError, run


COMMON_REPORT_TAGS = [reporting.Tags.SERVICES, reporting.Tags.TIME_MANAGEMENT]


def extract_tgz64(s):
    stream = io.BytesIO(base64.b64decode(s))
    tar = tarfile.open(fileobj=stream, mode='r:gz')
    tar.extractall('/')
    tar.close()


def enable_service(name):
    try:
        run(['systemctl', 'enable', '{}.service'.format(name)])
    except CalledProcessError:
        raise StopActorExecutionError('Could not enable {} service'.format(name))


def write_file(name, content):
    with open(name, 'w') as f:
        f.write(content)


def ntp2chrony(root, ntp_conf, step_tickers):
    # need to skip these on pylint to avoid "function already defined" if we move to the top of file
    from leapp.libraries.actor import ntp2chrony  # pylint: disable=import-outside-toplevel

    try:
        ntp_configuration = ntp2chrony.NtpConfiguration(root, ntp_conf, step_tickers)
        ntp_configuration.write_chrony_configuration('/etc/chrony.conf', '/etc/chrony.keys',
                                                     False, True)
    except Exception as e:
        raise StopActorExecutionError('ntp2chrony failed: {}'.format(e))

    # Return ignored lines from ntp.conf, except 'disable monitor' from
    # the default ntp.conf
    return set(ntp_configuration.ignored_lines) - set(['disable monitor'])


def migrate_ntp(migrate_services, config_tgz64):
    # Map of ntp->chrony services and flag if using configuration
    service_map = {'ntpd': ('chronyd', True),
                   'ntpdate': ('chronyd', True),
                   'ntp-wait': ('chrony-wait', False)}

    # Minimal secure ntp.conf with no sources to migrate ntpdate only
    no_sources_directives = (
            '# This file was created to migrate ntpdate configuration to chrony\n'
            '# without ntp configuration (ntpd service was disabled)\n'
            'driftfile /var/lib/ntp/drift\n'
            'restrict default ignore nomodify notrap nopeer noquery\n')

    if not migrate_services:
        # Nothing to migrate
        return

    migrate_configs = []
    for service in migrate_services:
        if service not in service_map:
            raise StopActorExecutionError('Unknown service {}'.format(service))
        enable_service(service_map[service][0])
        if service_map[service][1]:
            migrate_configs.append(service)

    # Unpack archive with configuration files
    extract_tgz64(config_tgz64)

    if 'ntpd' in migrate_configs:
        ntp_conf = '/etc/ntp.conf'
    else:
        ntp_conf = '/etc/ntp.conf.nosources'
        write_file(ntp_conf, no_sources_directives)

    step_tickers = '/etc/ntp/step-tickers' if 'ntpdate' in migrate_configs else ''

    ignored_lines = ntp2chrony('/', ntp_conf, step_tickers)

    config_resources = [reporting.RelatedResource('file', mc) for mc in migrate_configs + [ntp_conf]]
    package_resources = [reporting.RelatedResource('package', p) for p in ['ntpd', 'chrony']]

    if not ignored_lines:
        reporting.create_report([
            reporting.Title('{} configuration migrated to chrony'.format(' and '.join(migrate_configs))),
            reporting.Summary('ntp2chrony executed successfully'),
            reporting.Severity(reporting.Severity.INFO),
            reporting.Tags(COMMON_REPORT_TAGS)
        ] + config_resources + package_resources)

    else:
        reporting.create_report([
            reporting.Title('{} configuration partially migrated to chrony'.format(' and '.join(migrate_configs))),
            reporting.Summary('Some lines in /etc/ntp.conf were ignored in migration (check /etc/chrony.conf)'),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Tags(COMMON_REPORT_TAGS)
        ] + config_resources + package_resources)


# repos/system_upgrade/el7toel8/actors/migratentp/libraries/ntp2chrony.py

#!/usr/bin/python
#
# Convert ntp configuration to chrony
#
# Copyright (C) 2018-2019  Miroslav Lichvar <mlichvar@redhat.com>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


from __future__ import print_function

import argparse
import ipaddress
import logging
import os
import re
import subprocess
import sys

import six

# python2 compatibility hacks
if six.PY2:
    from io import open
    reload(sys)  # pylint: disable=undefined-variable # noqa: F821
    sys.setdefaultencoding("utf-8")


class NtpConfiguration(object):
    def __init__(self, root_dir, ntp_conf, step_tickers):
        self.root_dir = root_dir if root_dir != "/" else ""
        self.ntp_conf_path = ntp_conf
        self.step_tickers_path = step_tickers

        # Read and write files using an 8-bit transparent encoding
        self.file_encoding = "latin-1"
        self.enabled_services = set()
        self.step_tickers = []
        self.time_sources = []
        self.fudges = {}
        self.restrictions = {
                # Built-in defaults
                ipaddress.ip_network(u"0.0.0.0/0"): set(),
                ipaddress.ip_network(u"::/0"): set(),
        }
        self.keyfile = ""
        self.keys = []
        self.trusted_keys = []
        self.driftfile = ""
        self.statistics = []
        self.leapfile = ""
        self.tos_options = {}
        self.ignored_directives = set()
        self.ignored_lines = []

        # self.detect_enabled_services()
        self.parse_step_tickers()
        self.parse_ntp_conf()

    def detect_enabled_services(self):
        for service in ["ntpdate", "ntpd", "ntp-wait"]:
            service_path = os.path.join(self.root_dir,
                                        "etc/systemd/system/multi-user.target.wants/{}.service".format(service))
            if os.path.islink(service_path):
                self.enabled_services.add(service)
        logging.info("Enabled services found in /etc/systemd/system: %s",
                     " ".join(self.enabled_services))

    def parse_step_tickers(self):
        if not self.step_tickers_path:
            return

        path = os.path.join(self.root_dir, self.step_tickers_path)
        if not os.path.isfile(path):
            logging.info("Missing %s", path)
            return

        with open(path, encoding=self.file_encoding) as f:
            for line in f:
                line = line[:line.find('#')]

                words = line.split()

                if not words:
                    continue

                self.step_tickers.extend(words)

    def parse_ntp_conf(self, path=None):
        if path is None:
            path = os.path.join(self.root_dir, self.ntp_conf_path)

        with open(path, encoding=self.file_encoding) as f:
            logging.info("Reading %s", path)

            for line in f:
                line = line[:line.find('#')]

                words = line.split()

                if not words:
                    continue

                if not self.parse_directive(words):
                    self.ignored_lines.append(line)

    def parse_directive(self, words):
        res = True
        name = words.pop(0)
        if name.startswith("logconfig"):
            name = "logconfig"

        if words:
            if name in ["server", "peer", "pool"]:
                res = self.parse_source(name, words)
            elif name == "fudge":
                res = self.parse_fudge(words)
            elif name == "restrict":
                res = self.parse_restrict(words)
            elif name == "tos":
                res = self.parse_tos(words)
            elif name == "includefile":
                res = self.parse_includefile(words)
            elif name == "keys":
                res = self.parse_keys(words)
            elif name == "trustedkey":
                res = self.parse_trustedkey(words)
            elif name == "driftfile":
                self.driftfile = words[0]
            elif name == "statistics":
                self.statistics = words
            elif name == "leapfile":
                self.leapfile = words[0]
            else:
                self.ignored_directives.add(name)
                res = False
        else:
            self.ignored_directives.add(name)
            res = False

        return res

    def parse_source(self, source_type, words):
        ipv4_only = False
        ipv6_only = False
        source = {
                "type": source_type,
                "options": []
        }

        if words[0] == "-4":
            ipv4_only = True
            words.pop(0)
        elif words[0] == "-6":
            ipv6_only = True
            words.pop(0)

        if not words:
            return False

        source["address"] = words.pop(0)

        # Check if -4/-6 corresponds to the address and ignore hostnames
        if ipv4_only or ipv6_only:
            try:
                version = ipaddress.ip_address(source["address"]).version
                if (ipv4_only and version != 4) or (ipv6_only and version != 6):
                    return False
            except ValueError:
                return False

        if source["address"].startswith("127.127."):
            if not source["address"].startswith("127.127.1."):
                # Ignore non-LOCAL refclocks
                return False

        while words:
            if len(words) >= 2 and words[0] in ["minpoll", "maxpoll", "version", "key"]:
                source["options"].append((words[0], words[1]))
                words = words[2:]
            elif words[0] in ["burst", "iburst", "noselect", "prefer", "true", "xleave"]:
                source["options"].append((words[0],))
                words.pop(0)
            else:
                return False

        self.time_sources.append(source)
        return True

    def parse_fudge(self, words):
        address = words.pop(0)
        options = {}

        while words:
            if len(words) >= 2 and words[0] in ["stratum"]:
                if not words[1].isdigit():
                    return False
                options[words[0]] = int(words[1])
                words = words[2:]
            elif len(words) >= 2:
                words = words[2:]
            else:
                return False

        self.fudges[address] = options
        return True

    def parse_restrict(self, words):
        ipv4_only = False
        ipv6_only = False
        flags = set()
        mask = ""

        if words[0] == "-4":
            ipv4_only = True
            words.pop(0)
        elif words[0] == "-6":
            ipv6_only = True
            words.pop(0)

        if not words:
            return False

        address = words.pop(0)

        while words:
            if len(words) >= 2 and words[0] == "mask":
                mask = words[1]
                words = words[2:]
            else:
                if words[0] not in ["kod", "nomodify", "notrap", "nopeer", "noquery",
                                    "limited", "ignore", "noserve"]:
                    return False
                flags.add(words[0])
                words.pop(0)

        # Convert to IP network(s), ignoring restrictions with hostnames
        networks = []
        if address == "default" and not mask:
            if not ipv6_only:
                networks.append(ipaddress.ip_network(u"0.0.0.0/0"))
            if not ipv4_only:
                networks.append(ipaddress.ip_network(u"::/0"))
        else:
            try:
                if mask:
                    # Count bits in the mask (ipaddress does not support
                    # expanded IPv6 netmasks)
                    mask_ip = ipaddress.ip_address(mask)
                    mask_str = "{0:0{1}b}".format(int(mask_ip), mask_ip.max_prefixlen)
                    networks.append(ipaddress.ip_network(
                                    u"{}/{}".format(address, len(mask_str.rstrip('0')))))
                else:
                    networks.append(ipaddress.ip_network(address))
            except ValueError:
                return False

            if (ipv4_only and networks[-1].version != 4) or \
                    (ipv6_only and networks[-1].version != 6):
                return False

        for network in networks:
            self.restrictions[network] = flags

        return True

    def parse_tos(self, words):
        options = {}
        while words:
            if len(words) >= 2 and words[0] in ["minsane", "orphan"]:
                if not words[1].isdigit():
                    return False
                options[words[0]] = int(words[1])
                words = words[2:]
            elif len(words) >= 2 and words[0] in ["maxdist"]:
                # Check if it is a float value
                if not words[1].replace('.', '', 1).isdigit():
                    return False
                options[words[0]] = float(words[1])
                words = words[2:]
            else:
                return False

        self.tos_options.update(options)

        return True

    def parse_includefile(self, words):
        path = os.path.join(self.root_dir, words[0])
        if not os.path.isfile(path):
            return False

        self.parse_ntp_conf(path)
        return True

    def parse_keys(self, words):
        keyfile = words[0]
        path = os.path.join(self.root_dir, keyfile)
        if not os.path.isfile(path):
            logging.info("Missing %s", path)
            return False

        with open(path, encoding=self.file_encoding) as f:
            logging.info("Reading %s", path)
            keys = []
            for line in f:
                words = line.split()
                if len(words) < 3 or not words[0].isdigit():
                    continue
                keys.append((int(words[0]), words[1], words[2]))

            self.keyfile = keyfile
            self.keys = keys

        return True

    def parse_trustedkey(self, words):
        key_ranges = []
        for word in words:
            if word.isdigit():
                key_ranges.append((int(word), int(word)))
            elif re.match("^[0-9]+-[0-9]+$", word):
                first, last = word.split("-")
                key_ranges.append((int(first), int(last)))
            else:
                return False

        self.trusted_keys = key_ranges
        return True

    def write_chrony_configuration(self, chrony_conf_path, chrony_keys_path,
                                   dry_run=False, backup=False):
        chrony_conf = self.get_chrony_conf(chrony_keys_path)
        logging.debug("Generated %s:\n%s", chrony_conf_path, chrony_conf)

        if not dry_run:
            self.write_file(chrony_conf_path, 0o644, chrony_conf, backup)

        chrony_keys = self.get_chrony_keys()
        if chrony_keys:
            logging.debug("Generated %s:\n%s", chrony_keys_path, chrony_keys)

        if not dry_run:
            self.write_file(chrony_keys_path, 0o640, chrony_keys, backup)

    def get_processed_time_sources(self):
        # Convert {0,1,2,3}.*pool.ntp.org servers to 2.*pool.ntp.org pools

        # Make shallow copies of all sources (only type will be modified)
        time_sources = [s.copy() for s in self.time_sources]

        pools = {}
        for source in time_sources:
            if source["type"] != "server":
                continue
            m = re.match("^([0123])(\\.\\w+)?\\.pool\\.ntp\\.org$", source["address"])
            if m is None:
                continue
            number = m.group(1)
            zone = m.group(2)
            if zone not in pools:
                pools[zone] = []
            pools[zone].append((int(number), source))

        remove_servers = set()
        for zone, pool in pools.items():
            # sort and skip all pools not in [0, 3] range
            pool.sort()
            if [num for num, source in pool] != [0, 1, 2, 3]:
                # only exact group of 4 servers can be converted, nothing to do here
                continue
            # verify that parameters are the same for all servers in the pool
            if not all([p[1]["options"] == pool[0][1]["options"] for p in pool]):
                break
            remove_servers.update([pool[i][1]["address"] for i in [0, 1, 3]])
            pool[2][1]["type"] = "pool"

        processed_sources = []
        for source in time_sources:
            if source["type"] == "server" and source["address"] in remove_servers:
                continue
            processed_sources.append(source)
        return processed_sources

    def get_chrony_conf_sources(self):
        conf = ""

        if self.step_tickers:
            conf += "# Specify NTP servers used for initial correction.\n"
            conf += "initstepslew 0.1 {}\n".format(" ".join(self.step_tickers))
            conf += "\n"

        conf += "# Specify time sources.\n"

        for source in self.get_processed_time_sources():
            address = source["address"]
            if address.startswith("127.127."):
                if address.startswith("127.127.1."):
                    continue
                # No other refclocks are expected from the parser
                assert False
            else:
                conf += "{} {}".format(source["type"], address)
                for option in source["options"]:
                    if option[0] in ["minpoll", "maxpoll", "version", "key",
                                     "iburst", "noselect", "prefer", "xleave"]:
                        conf += " {}".format(" ".join(option))
                    elif option[0] == "burst":
                        conf += " presend 6"
                    elif option[0] == "true":
                        conf += " trust"
                    else:
                        # No other options are expected from the parser
                        assert False
                conf += "\n"
        conf += "\n"

        return conf

    def get_chrony_conf_allows(self):
        allowed_networks = [n for n in self.restrictions
                            if "ignore" not in self.restrictions[n] and "noserve" not in self.restrictions[n]]

        conf = ""
        for network in sorted(allowed_networks, key=lambda n: (n.version, n)):
            if network.num_addresses > 1:
                conf += "allow {}\n".format(network)
            else:
                conf += "allow {}\n".format(network.network_address)

        if conf:
            conf = "# Allow NTP client access.\n" + conf
            conf += "\n"

        return conf

    def get_chrony_conf_cmdallows(self):

        def _is_network_allowed(network):
            return ("ignore" not in self.restrictions[network] and "noquery" not in self.restrictions[network] and
                    network != ipaddress.ip_network(u"127.0.0.1/32") and network != ipaddress.ip_network(u"::1/128"))

        allowed_networks = [n for n in self.restrictions if _is_network_allowed(n)]

        ip_versions = set()
        conf = ""
        for network in sorted(allowed_networks, key=lambda n: (n.version, n)):
            ip_versions.add(network.version)
            if network.num_addresses > 1:
                conf += "cmdallow {}\n".format(network)
            else:
                conf += "cmdallow {}\n".format(network.network_address)

        if conf:
            conf = "# Allow remote monitoring.\n" + conf
            if 4 in ip_versions:
                conf += "bindcmdaddress 0.0.0.0\n"
            if 6 in ip_versions:
                conf += "bindcmdaddress ::\n"
            conf += "\n"

        return conf

    def get_chrony_conf(self, chrony_keys_path):
        local_stratum = 0
        maxdistance = 0.0
        minsources = 1
        orphan_stratum = 0
        logs = []

        for source in self.time_sources:
            address = source["address"]
            if address.startswith("127.127.1."):
                if address in self.fudges and "stratum" in self.fudges[address]:
                    local_stratum = self.fudges[address]["stratum"]
                else:
                    local_stratum = 5

        if "maxdist" in self.tos_options:
            maxdistance = self.tos_options["maxdist"]
        if "minsane" in self.tos_options:
            minsources = self.tos_options["minsane"]
        if "orphan" in self.tos_options:
            orphan_stratum = self.tos_options["orphan"]

        if "clockstats" in self.statistics:
            logs.append("refclocks")
        if "loopstats" in self.statistics:
            logs.append("tracking")
        if "peerstats" in self.statistics:
            logs.append("statistics")
        if "rawstats" in self.statistics:
            logs.append("measurements")

        conf = "# This file was converted from {}{}.\n".format(
                    self.ntp_conf_path,
                    " and " + self.step_tickers_path if self.step_tickers_path else "")
        conf += "\n"

        if self.ignored_lines:
            conf += "# The following directives were ignored in the conversion:\n"

            for line in self.ignored_lines:
                # Remove sensitive information
                line = re.sub(r"\s+pw\s+\S+", " pw XXX", line.rstrip())
                conf += "# " + line + "\n"
            conf += "\n"

        conf += self.get_chrony_conf_sources()

        conf += "# Record the rate at which the system clock gains/losses time.\n"
        if not self.driftfile:
            conf += "#"
        conf += "driftfile /var/lib/chrony/drift\n"
        conf += "\n"

        conf += "# Allow the system clock to be stepped in the first three updates\n"
        conf += "# if its offset is larger than 1 second.\n"
        conf += "makestep 1.0 3\n"
        conf += "\n"

        conf += "# Enable kernel synchronization of the real-time clock (RTC).\n"
        conf += "rtcsync\n"
        conf += "\n"

        conf += "# Enable hardware timestamping on all interfaces that support it.\n"
        conf += "#hwtimestamp *\n"
        conf += "\n"

        if maxdistance > 0.0:
            conf += "# Specify the maximum distance of sources to be selectable.\n"
            conf += "maxdistance {}\n".format(maxdistance)
            conf += "\n"

        conf += "# Increase the minimum number of selectable sources required to adjust\n"
        conf += "# the system clock.\n"
        if minsources > 1:
            conf += "minsources {}\n".format(minsources)
        else:
            conf += "#minsources 2\n"
        conf += "\n"

        conf += self.get_chrony_conf_allows()

        conf += self.get_chrony_conf_cmdallows()

        conf += "# Serve time even if not synchronized to a time source.\n"
        if 0 < orphan_stratum < 16:
            conf += "local stratum {} orphan\n".format(orphan_stratum)
        elif 0 < local_stratum < 16:
            conf += "local stratum {}\n".format(local_stratum)
        else:
            conf += "#local stratum 10\n"
        conf += "\n"

        conf += "# Specify file containing keys for NTP authentication.\n"
        conf += "keyfile {}\n".format(chrony_keys_path)
        conf += "\n"

        conf += "# Get TAI-UTC offset and leap seconds from the system tz database.\n"
        conf += "leapsectz right/UTC\n"
        conf += "\n"

        conf += "# Specify directory for log files.\n"
        conf += "logdir /var/log/chrony\n"
        conf += "\n"

        conf += "# Select which information is logged.\n"
        if logs:
            conf += "log {}\n".format(" ".join(logs))
        else:
            conf += "#log measurements statistics tracking\n"

        return conf

    def get_chrony_keys(self):
        if not self.keyfile:
            return ""

        keys = "# This file was converted from {}.\n".format(self.keyfile)
        keys += "\n"

        for key in self.keys:
            key_id = key[0]
            key_type = key[1]
            password = key[2]

            if key_type in ["m", "M"]:
                key_type = "MD5"
            elif key_type not in ["MD5", "SHA1", "SHA256", "SHA384", "SHA512"]:
                continue

            prefix = "ASCII" if len(password) <= 20 else "HEX"

            for first, last in self.trusted_keys:
                if first <= key_id <= last:
                    trusted = True
                    break
            else:
                trusted = False

            # Disable keys that were not marked as trusted
            if not trusted:
                keys += "#"

            keys += "{key_id} {key_type} {prefix}:{password}\n".format(
                    key_id=key_id, key_type=key_type, prefix=prefix, password=password)

        return keys

    def write_file(self, path, mode, content, backup):
        path = self.root_dir + path
        if backup and os.path.isfile(path):
            os.rename(path, path + ".old")

        with open(os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL, mode), "w",
                  encoding=self.file_encoding) as f:
            logging.info("Writing %s", path)
            f.write(u"" + content)

        # Fix SELinux context if restorecon is installed
        try:
            subprocess.call(["restorecon", path])
        except OSError:
            pass


def main():
    parser = argparse.ArgumentParser(description="Convert ntp configuration to chrony.")
    parser.add_argument("-r", "--root", dest="roots", default=["/"], nargs="+",
                        metavar="DIR", help="specify root directory (default /)")
    parser.add_argument("--ntp-conf", action="store", default="/etc/ntp.conf",
                        metavar="FILE", help="specify ntp config (default /etc/ntp.conf)")
    parser.add_argument("--step-tickers", action="store", default="",
                        metavar="FILE", help="specify ntpdate step-tickers config (no default)")
    parser.add_argument("--chrony-conf", action="store", default="/etc/chrony.conf",
                        metavar="FILE", help="specify chrony config (default /etc/chrony.conf)")
    parser.add_argument("--chrony-keys", action="store", default="/etc/chrony.keys",
                        metavar="FILE", help="specify chrony keyfile (default /etc/chrony.keys)")
    parser.add_argument("-b", "--backup", action="store_true", help="backup existing configs before writing")
    parser.add_argument("-L", "--ignored-lines", action="store_true", help="print ignored lines")
    parser.add_argument("-D", "--ignored-directives", action="store_true",
                        help="print names of ignored directives")
    parser.add_argument("-n", "--dry-run", action="store_true", help="don't make any changes")
    parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")

    args = parser.parse_args()

    logging.basicConfig(format="%(message)s",
                        level=[logging.ERROR, logging.INFO, logging.DEBUG][min(args.verbose, 2)])

    for root in args.roots:
        conf = NtpConfiguration(root, args.ntp_conf, args.step_tickers)

        if args.ignored_lines:
            for line in conf.ignored_lines:
                print(line)

        if args.ignored_directives:
            for directive in conf.ignored_directives:
                print(directive)

        conf.write_chrony_configuration(args.chrony_conf, args.chrony_keys, args.dry_run, args.backup)


if __name__ == "__main__":
    main()


# repos/system_upgrade/el7toel8/actors/migratentp/tests/test_converter_migratentp.py

import os

import pytest

from leapp.libraries.actor import ntp2chrony

CUR_DIR = os.path.dirname(os.path.abspath(__file__))

NTP_CONF = os.path.join(CUR_DIR, "data/ntp.conf")
STEP_TICKERS = os.path.join(CUR_DIR, "data/step_tickers")

# TODO [Artem] the following consts should use abs path as well.
#   reader of [[:digit:]]chrony.conf files does not support wildcards, so we
#   have to change the working directory here for now.
NTP_MATCH_DIR = "data/ntpconfs/"
CHRONY_MATCH_DIR = "data/chronyconfs/"


@pytest.fixture
def adjust_cwd():
    previous_cwd = os.getcwd()
    os.chdir(CUR_DIR)
    yield
    os.chdir(previous_cwd)


class TestConverter(object):
    def test_basic(self):
        config = ntp2chrony.NtpConfiguration(CUR_DIR, NTP_CONF, step_tickers=STEP_TICKERS)
        present = [config.restrictions, config.driftfile, config.trusted_keys, config.keys,
                   config.step_tickers, config.restrictions]
        for section in present:
            assert section
        chrony_conf = config.get_chrony_conf('/etc/chrony.keys')
        # additional verification section by section for each param in present?

        # verify step_tickers -> initstepslew
        initstepslew_line = next((l for l in chrony_conf.split('\n')
                                  if l.startswith('initstepslew')), None)
        assert initstepslew_line and initstepslew_line.endswith(' '.join(config.step_tickers))
        chrony_keys = config.get_chrony_keys()
        # verify keys generation
        for num, _, key in config.keys:
            expected = ('%(num)s MD5 %(key)s' %
                        {'key': 'HEX:' if len(key) > 20 else 'ASCII:' + key, 'num': num})
            # keys not from trusted keys are commented out by default
            if not any(num in range(x, y + 1) for (x, y) in config.trusted_keys):
                expected = '#' + expected
            assert expected in chrony_keys


class TestConfigConversion(object):
    def _do_match(self, expected_file, actual):
        expected_lines = []
        actual_lines = []
        with open(expected_file) as f:
            expected_lines = [l.strip() for l in f.readlines()
                              if l.strip() and not l.strip().startswith('#')]
        actual_lines = [l.strip() for l in actual.split('\n')
                        if l.strip() and not l.strip().startswith('#')]
        assert expected_lines == actual_lines

    def _check_existance(self, fname, default=''):
        if os.path.exists(fname):
            return fname
        return default

    def test_match(self, adjust_cwd):

        for f in [fe for fe in os.listdir(NTP_MATCH_DIR) if fe.endswith('conf')]:
            # get recorded actual result
            num = f.split('.')[0].split('_')[0]
            ntp_conf = os.path.join(NTP_MATCH_DIR, f)
            step_tickers = self._check_existance(
                os.path.join(NTP_MATCH_DIR, '%s_step_tickers' % num))
            config = ntp2chrony.NtpConfiguration('',
                                                 ntp_conf,
                                                 step_tickers=step_tickers)
            potential_chrony_keys = os.path.join(CHRONY_MATCH_DIR, "%s_chrony.keys" % num)
            actual_data = config.get_chrony_conf(chrony_keys_path=potential_chrony_keys)
            expected_fname = os.path.join(CHRONY_MATCH_DIR, "%s_chrony.conf" % num)
            # make sure recorded and generated configs match
            self._do_match(expected_fname, actual_data)
            actual_keys = config.get_chrony_keys()
            expected_keys_file = self._check_existance(potential_chrony_keys)
            # if keys are recorded or generated make sure they match
            if actual_keys and expected_keys_file != '':
                self._do_match(expected_keys_file, actual_keys)


# repos/system_upgrade/el7toel8/actors/migratentp/tests/unit_test_migratentp.py

from leapp.libraries.actor import migratentp
from leapp import reporting
from leapp.libraries.common.testutils import create_report_mocked


class extract_tgz64_mocked(object):
    def __init__(self):
        self.called = 0
        self.s = None

    def __call__(self, s):
        self.called += 1
        self.s = s


class enable_service_mocked(object):
    def __init__(self):
        self.called = 0
        self.names = []

    def __call__(self, name):
        self.called += 1
        self.names.append(name)


class write_file_mocked(object):
    def __init__(self):
        self.called = 0
        self.name = None
        self.content = None

    def __call__(self, name, content):
        self.called += 1
        self.name = name
        self.content = content


class ntp2chrony_mocked(object):
    def __init__(self, lines):
        self.called = 0
        self.ignored_lines = lines
        self.args = None

    def __call__(self, *args):
        self.called += 1
        self.args = args
        return self.ignored_lines * ['a line']


def test_migration(monkeypatch):
    for ntp_services, chrony_services, ignored_lines in [
                ([], [], 0),
                (['ntpd'], ['chronyd'], 0),
                (['ntpdate'], ['chronyd'], 1),
                (['ntp-wait'], ['chrony-wait'], 0),
                (['ntpd', 'ntpdate', 'ntp-wait'], ['chronyd', 'chronyd', 'chrony-wait'], 1),
            ]:
        monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
        monkeypatch.setattr(migratentp, 'extract_tgz64', extract_tgz64_mocked())
        monkeypatch.setattr(migratentp, 'enable_service', enable_service_mocked())
        monkeypatch.setattr(migratentp, 'write_file', write_file_mocked())
        monkeypatch.setattr(migratentp, 'ntp2chrony', ntp2chrony_mocked(ignored_lines))

        migratentp.migrate_ntp(ntp_services, 'abcdef')

        if ntp_services:
            assert reporting.create_report.called == 1
            if ignored_lines > 0:
                assert 'configuration partially migrated to chrony' in \
                        reporting.create_report.report_fields['title']
            else:
                assert 'configuration migrated to chrony' in \
                        reporting.create_report.report_fields['title']

            assert migratentp.extract_tgz64.called == 1
            assert migratentp.extract_tgz64.s == 'abcdef'
            assert migratentp.enable_service.called == len(chrony_services)
            assert migratentp.enable_service.names == chrony_services
            assert migratentp.write_file.called == (0 if 'ntpd' in ntp_services else 1)
            if migratentp.write_file.called:
                assert migratentp.write_file.name == '/etc/ntp.conf.nosources'
                assert 'without ntp configuration' in migratentp.write_file.content
            assert migratentp.ntp2chrony.called == 1
            assert migratentp.ntp2chrony.args == (
                    '/',
                    '/etc/ntp.conf' if 'ntpd' in ntp_services else '/etc/ntp.conf.nosources',
                    '/etc/ntp/step-tickers' if 'ntpdate' in ntp_services else '')
        else:
            assert reporting.create_report.called == 0
            assert migratentp.extract_tgz64.called == 0
            assert migratentp.enable_service.called == 0
            assert migratentp.write_file.called == 0
            assert migratentp.ntp2chrony.called == 0


# repos/system_upgrade/el7toel8/actors/migratesendmail/libraries/migratesendmail.py

import ipaddress
import os
import re
import shutil

from six import text_type

BackupSuffix = '.bak'

# false positives blacklist
rfp = re.compile(r'(^\s*RIPv6:::1\b)|(@\s+\[IPv6:::1\]\s+>)')

rs = re.compile(r'IPv6:[0-9a-fA-F:]*::[0-9a-fA-F:]*')


def uncompress_ipv6(ipv6):
    addr = text_type(ipv6.replace('IPv6:', ''))
    try:
        addr = 'IPv6:' + ipaddress.ip_address(addr).exploded
    except ValueError:
        addr = ipv6
    return re.sub(r':0([^:])', r':\1', re.sub(r'0+', r'0', addr))


def check_false_positives(f, l):
    return f in ['sendmail.cf', 'submit.cf'] and rfp.search(l) is not None


def sub_ipv6(m):
    return uncompress_ipv6(m.group(0))


def migrate_file(fn):
    # make backup
    shutil.copy2(fn, fn + BackupSuffix)
    with open(fn, 'w') as file_out:
        with open(fn + BackupSuffix) as file_in:
            for line in file_in:
                if rs.search(line) and not check_false_positives(os.path.basename(fn), line):
                    line = rs.sub(sub_ipv6, line)
                file_out.write(line)


# repos/system_upgrade/el7toel8/actors/migratesendmail/tests/component_test_migratesendmail.py

import os

from six import text_type

from leapp.models import SendmailMigrationDecision
from leapp.reporting import Report


def test_actor_migration(tmpdir, current_actor_context):
    test_cfg_file = text_type(tmpdir.join('sendmail.cf'))
    with open(test_cfg_file, 'w') as file_out:
        file_out.write("IPv6:::1")
    current_actor_context.feed(SendmailMigrationDecision(migrate_files=[test_cfg_file]))
    current_actor_context.run()
    with open(test_cfg_file, 'r') as file_in:
        data = file_in.read()
    assert data == 'IPv6:0:0:0:0:0:0:0:1'


# repos/system_upgrade/el7toel8/actors/multipathconfcheck/libraries/multipathconfcheck.py

from leapp import reporting
from leapp.reporting import create_report


def _merge_configs(configs):
    options = {'default_path_checker': None, 'detect_prio': None,
               'detect_path_checker': None, 'reassign_maps': None,
               'retain_attached_hw_handler': None}
    for config in configs:
        if config.default_path_checker is not None:
            options['default_path_checker'] = (config.default_path_checker,
                                               config.pathname)

        if config.reassign_maps is not None:
            options['reassign_maps'] = (config.reassign_maps, config.pathname)

        if config.default_detect_checker is not None:
            options['detect_path_checker'] = (config.default_detect_checker,
                                              config.pathname)

        if config.default_detect_prio is not None:
            options['detect_prio'] = (config.default_detect_prio,
                                      config.pathname)

        if config.default_retain_hwhandler is not None:
            options['retain_attached_hw_handler'] = (config.default_retain_hwhandler, config.pathname)
    return options


def _check_default_path_checker(options):
    if not options['default_path_checker']:
        return
    value, pathname = options['default_path_checker']
    if value == 'tur':
        return
    create_report([
        reporting.Title(
            'Unsupported device-mapper-multipath configuration'
        ),
        reporting.Summary(
            'device-mapper-multipath has changed the default path_checker '
            'from "directio" to "tur" in RHEL-8. Further, changing the '
            'default path_checker can cause issues with built-in device '
            'configurations in RHEL-8. Please remove the "path_checker" '
            'option from the defaults section of {}, and add it to the '
            'device configuration of any devices that need it.'.
            format(pathname)
        ),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Tags([reporting.Tags.SERVICES]),
        reporting.Flags([reporting.Flags.INHIBITOR]),
        reporting.RelatedResource('package', 'device-mapper-multipath'),
        reporting.RelatedResource('file', pathname),
        reporting.Remediation(
            hint='Please remove the "path_checker {}" option from the '
            'defaults section of {}, and add it to the device configuration '
            'of any devices that need it.'.format(value, pathname)
        )
    ])


def _create_paths_str(paths):
    if len(paths) < 2:
        return paths[0]
    return '{} and {}'.format(', '.join(paths[0:-1]), paths[-1])


def _check_default_detection(options):
    bad = []
    for keyword in ('detect_path_checker', 'detect_prio',
                    'retain_attached_hw_handler'):
        if options[keyword] and not options[keyword][0] and \
                options[keyword][1] not in bad:
            bad.append(options[keyword][1])
    if not bad:
        return
    paths = _create_paths_str(bad)
    create_report([
        reporting.Title(
            'device-mapper-multipath now defaults to detecting settings'
        ),
        reporting.Summary(
            'In RHEL-8, the default value for the "detect_path_checker", '
            '"detect_prio" and "retain_attached_hw_handler" options has '
            'changed to "yes". Further, changing these default values can '
            'cause issues with the built-in device configurations in RHEL-8. '
            'They will be commented out in the defaults section of all '
            'multipath config files. This is unlikely to cause any issues '
            'with existing configurations. If it does, please move these '
            'options from the defaults sections of {} to the device '
            'configuration sections of any devices that need them.'.
            format(paths)
        ),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Tags([reporting.Tags.SERVICES]),
        reporting.RelatedResource('package', 'device-mapper-multipath')
    ])


def _check_reassign_maps(options):
    if not options['reassign_maps']:
        return
    value, pathname = options['reassign_maps']
    if not value:
        return
    create_report([
        reporting.Title(
            'device-mapper-multipath now disables reassign_maps by default'
        ),
        reporting.Summary(
            'In RHEL-8, the default value for "reassign_maps" has been '
            'changed to "no", and it is not recommended to enable it in any '
            'configuration going forward. This option will be commented out '
            'in {}.'.format(pathname)
        ),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Tags([reporting.Tags.SERVICES]),
        reporting.RelatedResource('package', 'device-mapper-multipath')
    ])


def check_configs(facts):
    options = _merge_configs(facts.configs)
    _check_default_path_checker(options)
    _check_default_detection(options)
    _check_reassign_maps(options)


# repos/system_upgrade/el7toel8/actors/multipathconfcheck/tests/test_actor_multipathconfcheck.py

from leapp.models import (
    MultipathConfFacts,
    MultipathConfig,
    MultipathConfigOption
)
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context


def _assert_default_checker_report(report, pathname):
    assert report['title'] == \
        'Unsupported device-mapper-multipath configuration'
    assert report['severity'] == 'high'
    assert 'inhibitor' in report['flags']
    assert pathname in report['summary']


def _assert_default_detect_report(report, pathname):
    assert report['title'] == \
        'device-mapper-multipath now defaults to detecting settings'
    assert report['severity'] == 'medium'
    assert pathname in report['summary']


def _assert_reassign_maps(report, pathname):
    assert report['title'] == \
        'device-mapper-multipath now disables reassign_maps by default'
    assert report['severity'] == 'medium'
    assert pathname in report['summary']


def test_config_all_bad(current_actor_context):
    config = MultipathConfig(
            pathname='all_bad.conf', default_path_checker='directio',
            reassign_maps=True, default_detect_checker=False,
            default_detect_prio=False, default_retain_hwhandler=False)
    facts = MultipathConfFacts(configs=[config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 3
    _assert_default_checker_report(reports[0].report, 'all_bad.conf')
    _assert_default_detect_report(reports[1].report, 'all_bad.conf')
    _assert_reassign_maps(reports[2].report, 'all_bad.conf')


def test_config_all_good(current_actor_context):
    config = MultipathConfig(
            pathname='all_good.conf', default_path_checker='tur',
            reassign_maps=False, default_detect_checker=True,
            default_detect_prio=True, default_retain_hwhandler=True)
    facts = MultipathConfFacts(configs=[config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_config_unimportant(current_actor_context):
    option = MultipathConfigOption(name='path_checker', value='rdac')
    config = MultipathConfig(
            pathname='unimportant.conf', hw_str_match_exists=True,
            ignore_new_boot_devs_exists=True, new_bindings_in_boot_exists=True,
            unpriv_sgio_exists=True, detect_path_checker_exists=True,
            overrides_hwhandler_exists=True, overrides_pg_timeout_exists=True,
            queue_if_no_path_exists=True, all_devs_section_exists=True,
            all_devs_options=[option])
    facts = MultipathConfFacts(configs=[config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_bad_then_good(current_actor_context):
    bad_config = MultipathConfig(
            pathname='all_bad.conf', default_path_checker='directio',
            reassign_maps=True, default_detect_checker=False,
            default_detect_prio=False, default_retain_hwhandler=False)
    good_config = MultipathConfig(
            pathname='all_good.conf', default_path_checker='tur',
            reassign_maps=False, default_detect_checker=True,
            default_detect_prio=True, default_retain_hwhandler=True)
    facts = MultipathConfFacts(configs=[bad_config, good_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_good_then_bad(current_actor_context):
    good_config = MultipathConfig(
            pathname='all_good.conf', default_path_checker='tur',
            reassign_maps=False, default_detect_checker=True,
            default_detect_prio=True, default_retain_hwhandler=True)
    bad_config = MultipathConfig(
            pathname='all_bad.conf', default_path_checker='directio',
            reassign_maps=True, default_detect_checker=False,
            default_detect_prio=False, default_retain_hwhandler=False)
    facts = MultipathConfFacts(configs=[good_config, bad_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 3
    _assert_default_checker_report(reports[0].report, 'all_bad.conf')
    _assert_default_detect_report(reports[1].report, 'all_bad.conf')
    _assert_reassign_maps(reports[2].report, 'all_bad.conf')


def test_bad_then_nothing(current_actor_context):
    bad_config = MultipathConfig(
            pathname='all_bad.conf', default_path_checker='directio',
            reassign_maps=True, default_detect_checker=False,
            default_detect_prio=False, default_retain_hwhandler=False)
    none_config = MultipathConfig(pathname='none.conf')
    facts = MultipathConfFacts(configs=[bad_config, none_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 3
    _assert_default_checker_report(reports[0].report, 'all_bad.conf')
    _assert_default_detect_report(reports[1].report, 'all_bad.conf')
    _assert_reassign_maps(reports[2].report, 'all_bad.conf')


def test_nothing_then_bad(current_actor_context):
    bad_config = MultipathConfig(
            pathname='all_bad.conf', default_path_checker='directio',
            reassign_maps=True, default_detect_checker=False,
            default_detect_prio=False, default_retain_hwhandler=False)
    none_config = MultipathConfig(pathname='none.conf')
    facts = MultipathConfFacts(configs=[none_config, bad_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 3
    _assert_default_checker_report(reports[0].report, 'all_bad.conf')
    _assert_default_detect_report(reports[1].report, 'all_bad.conf')
    _assert_reassign_maps(reports[2].report, 'all_bad.conf')


def test_only_bad_checker(current_actor_context):
    bad_checker_config = MultipathConfig(
            pathname='bad_checker.conf', default_path_checker='rdac',
            default_retain_hwhandler=True)
    facts = MultipathConfFacts(configs=[bad_checker_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 1
    _assert_default_checker_report(reports[0].report, 'bad_checker.conf')


def test_only_bad_detect(current_actor_context):
    bad_detect_config = MultipathConfig(
            pathname='bad_detect.conf', default_detect_prio=True,
            default_detect_checker=False)
    facts = MultipathConfFacts(configs=[bad_detect_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 1
    _assert_default_detect_report(reports[0].report, 'bad_detect.conf')


def test_only_bad_reassign(current_actor_context):
    bad_reassign_config = MultipathConfig(
            pathname='bad_reassign.conf', reassign_maps=True)
    facts = MultipathConfFacts(configs=[bad_reassign_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 1
    _assert_reassign_maps(reports[0].report, 'bad_reassign.conf')


def test_different_files(current_actor_context):
    bad_detect_checker_config = MultipathConfig(
            pathname='bad_detect_checker.conf', default_detect_checker=False)
    bad_detect_prio_config = MultipathConfig(
            pathname='bad_detect_prio.conf', default_detect_prio=False)
    bad_retain_hwhandler_config = MultipathConfig(
            pathname='bad_retain_hwhandler.conf',
            default_retain_hwhandler=False)
    facts = MultipathConfFacts(
            configs=[bad_detect_checker_config, bad_detect_prio_config,
                     bad_retain_hwhandler_config])

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports and len(reports) == 1
    _assert_default_detect_report(
            reports[0].report,
            'bad_detect_checker.conf, bad_detect_prio.conf and '
            'bad_retain_hwhandler.conf')


# repos/system_upgrade/el7toel8/actors/multipathconfupdate/libraries/multipathconfupdate.py

import re

from leapp.libraries.common import multipathutil

_bool_options = {'retain_attached_hw_handler': True, 'detect_prio': True,
                 'detect_path_checker': True, 'reassign_maps': False}

_exist_options = ('hw_str_match', 'ignore_new_boot_devs',
                  'new_bindings_in_boot', 'unpriv_sgio')

_ovr_options = ('hardware_handler', 'pg_timeout')


class _QueueIfNoPathInfo(object):
    def __init__(self, line, value):
        self.line = line
        self.value = value
        self.has_no_path_retry = False


def _nothing_to_do(config):
    if config.default_path_checker and config.default_path_checker != 'tur':
        return False

    config_checks = (
        (config.default_retain_hwhandler, False),
        (config.default_detect_prio, False),
        (config.default_detect_checker, False),
        (config.reassign_maps, True),
        (config.hw_str_match_exists, True),
        (config.ignore_new_boot_devs_exists, True),
        (config.new_bindings_in_boot_exists, True),
        (config.unpriv_sgio_exists, True),
        (config.detect_path_checker_exists, True),
        (config.overrides_hwhandler_exists, True),
        (config.overrides_pg_timeout_exists, True),
        (config.queue_if_no_path_exists, True),
        (config.all_devs_section_exists, True)
    )
    for option, value in config_checks:
        if option is value:
            return False

    return config.all_devs_options == []


def _comment_out_line(line):
    return '# ' + line + ' # Commented out by Leapp'


def _comment_out_ranges(lines, ranges):
    for start, end in ranges:
        line = lines[start]
        lines[start] = '# ' + line + ' # Section commented out by Leapp'
        for i in range(start + 1, end):
            line = lines[i]
            if line == '':
                lines[i] = '#'
            elif line[0] != '#':
                lines[i] = '# ' + line


def _setup_value(value):
    if re.search(r'\s', value):
        return '"' + value + '"'
    return value


def _add_overrides(lines, options):
    lines.append('overrides { # Section added by Leapp')
    for option in options:
        lines.append('\t{} {}'.format(option.name, _setup_value(option.value)))
    lines.append('}')
    lines.append('')


def _update_overrides(lines, ovr_line, options):
    new_lines = []
    start = None
    for i, line in enumerate(lines):
        if line is ovr_line:
            start = i + 1
            break
    if not start:
        return
    for option in options:
        new_lines.append('\t{} {} # Line added by Leapp'.
                         format(option.name, _setup_value(option.value)))
    lines[start:start] = new_lines      # insert new_lines


def _convert_checker_line(line):
    return line.replace('detect_path_checker', 'detect_checker') + \
            ' # Line modified by Leapp'


def _modify_features_line(line, value):
    items = value.split()
    if items == [] or not items[0].isdigit():
        return _comment_out_line(line)
    nr_features = int(items[0])
    if nr_features != len(items) - 1:
        return _comment_out_line(line)
    r = re.match('^(.*)features', line)
    if not r:
        return _comment_out_line(line)
    line_start = r.group(1)
    try:
        items.remove('queue_if_no_path')
    except ValueError:
        return _comment_out_line(line)
    items[0] = str(nr_features - 1)
    return line_start + 'features "' + ' '.join(items) + \
        '" # Line modified by Leapp'


def _add_npr(lines, line, i):
    r = re.match('^(.*)features', line)
    if not r:
        return
    line_start = r.group(1)
    lines.insert(i,
                 line_start + 'no_path_retry queue # Line added by Leapp')


def _remove_qinp(lines, qinp_infos):
    infos_iter = iter(qinp_infos)
    info = next(infos_iter, None)
    if not info:
        return
    i = 0
    while i < len(lines):
        if lines[i] is info.line:
            lines[i] = _modify_features_line(info.line, info.value)
            if not info.has_no_path_retry:
                _add_npr(lines, info.line, i + 1)
            info = next(infos_iter, None)
            if not info:
                return
        i += 1


def _valid_npr(value):
    if value.isdigit() and int(value) >= 0:
        return True
    if value in ('fail', 'queue'):
        return True
    return False


def _update_config(config):
    if _nothing_to_do(config):
        return None
    contents = multipathutil.read_config(config.pathname)
    if contents is None:
        return None
    lines = contents.split('\n')
    section = None
    in_subsection = False
    in_all_devs = False
    subsection_start = None
    all_devs_ranges = []
    overrides_line = None
    qinp_info = None
    has_no_path_retry = False
    qinp_infos = []
    for i, line in enumerate(lines):
        try:
            data = multipathutil.LineData(line, section, in_subsection)
        except ValueError:
            continue
        if data.type == data.TYPE_SECTION_END:
            if qinp_info and not in_all_devs:
                qinp_info.has_no_path_retry = has_no_path_retry
                qinp_infos.append(qinp_info)
            qinp_info = None
            has_no_path_retry = False
            if in_subsection:
                in_subsection = False
                if in_all_devs:
                    all_devs_ranges.append((subsection_start, i + 1))
                in_all_devs = False
                subsection_start = None
            elif section is not None:
                section = None
        elif data.type == data.TYPE_SECTION_START:
            if section is None:
                section = data.section
                if section == 'overrides':
                    overrides_line = line
            elif not in_subsection:
                in_subsection = True
                subsection_start = i
        if data.type != data.TYPE_OPTION:
            continue
        if section == 'defaults':
            if (data.option in ('path_checker', 'checker')) and data.value != 'tur':
                lines[i] = _comment_out_line(line)
                continue
            if data.option in _bool_options and \
                    _bool_options[data.option] != data.is_enabled():
                lines[i] = _comment_out_line(line)
                continue
        elif section == 'overrides' and data.option in _ovr_options:
            lines[i] = _comment_out_line(line)
            continue
        elif section == 'devices' and in_subsection and \
                data.option == 'all_devs' and data.is_enabled():
            in_all_devs = True
            continue
        if data.option in _exist_options:
            lines[i] = _comment_out_line(line)
        elif data.option == 'detect_path_checker':
            lines[i] = _convert_checker_line(line)
        elif data.option == 'no_path_retry' and _valid_npr(data.value):
            has_no_path_retry = True
        elif data.option == 'features' and 'queue_if_no_path' in data.value:
            qinp_info = _QueueIfNoPathInfo(line, data.value)

    if in_subsection:
        lines.append('\t} # line added by Leapp')
        if in_all_devs:
            all_devs_ranges.append((subsection_start, len(lines)))
        elif qinp_info:
            qinp_info.has_no_path_retry = has_no_path_retry
            qinp_infos.append(qinp_info)
            qinp_info = None
    if section is not None:
        lines.append('} # line added by Leapp')
        lines.append('')
        if qinp_info:
            qinp_info.has_no_path_retry = has_no_path_retry
            qinp_infos.append(qinp_info)
    _comment_out_ranges(lines, all_devs_ranges)
    if qinp_infos:
        _remove_qinp(lines, qinp_infos)
    if config.all_devs_options != []:
        if overrides_line:
            _update_overrides(lines, overrides_line, config.all_devs_options)
        else:
            _add_overrides(lines, config.all_devs_options)
    contents = '\n'.join(lines)
    return contents


def update_configs(facts):
    for config in facts.configs:
        contents = _update_config(config)
        if contents:
            multipathutil.write_config(config.pathname, contents)


# repos/system_upgrade/el7toel8/actors/multipathconfupdate/tests/test_library_multipathconfupdate.py

import os

from leapp.libraries.actor import multipathconfupdate
from leapp.libraries.common import multipathutil
from leapp.models import MultipathConfig, MultipathConfigOption

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def build_config(val):
    all_devs_options_val = []
    for name_val, value_val in val[16]:
        option = MultipathConfigOption(name=name_val, value=value_val)
        all_devs_options_val.append(option)
    return MultipathConfig(
            pathname=val[0],
            default_path_checker=val[1],
            config_dir=val[2],
            default_retain_hwhandler=val[3],
            default_detect_prio=val[4],
            default_detect_checker=val[5],
            reassign_maps=val[6],
            hw_str_match_exists=val[7],
            ignore_new_boot_devs_exists=val[8],
            new_bindings_in_boot_exists=val[9],
            unpriv_sgio_exists=val[10],
            detect_path_checker_exists=val[11],
            overrides_hwhandler_exists=val[12],
            overrides_pg_timeout_exists=val[13],
            queue_if_no_path_exists=val[14],
            all_devs_section_exists=val[15],
            all_devs_options=all_devs_options_val)


default_rhel7_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/default_rhel7.conf'), 'directio', os.path.join(CUR_DIR, 'files/conf.d'),
     False, False, False, True, True, True, True, True, True, False, False, True, False, [], ])

all_devs_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/all_devs.conf'), None, None, None, None, None, None, False, False, False,
     True, True, False, False, True, True,
     [('path_checker', 'rdac'), ('detect_checker', 'yes'), ('features', '2 pg_init_retries 50'),
      ('path_selector', 'service-time 0'), ('fast_io_fail_tmo', '5'), ('no_path_retry', 'queue'), ], ])

empty_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/empty.conf'), None, None, None, None, None, None, False, False, False, False,
     False, False, False, False, False, [], ])

default_rhel8_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/default_rhel8.conf'), 'tur', '/etc/multipath/conf.d', True, True, None, False,
     False, False, False, False, False, False, False, False, False, [], ])

all_the_things_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/all_the_things.conf'), 'directio', os.path.join(CUR_DIR, 'files/conf.d'),
     False, False, False, True, True, True, True, True, True, True, True, True, True,
     [('no_path_retry', 'fail'), ('features', '0')], ])

already_updated_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/already_updated.conf'), None, os.path.join(CUR_DIR, 'files/conf.d'), None,
     None, None, None, False, False, False, False, False, False, False, False, False, [], ])

ugly1_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/ugly1.conf'), 'directio', os.path.join(CUR_DIR, 'files/conf.d'), False, False,
     False, True, True, True, True, True, True, True, True, True, True,
     [('dev_loss_tmo', '60'), ('path_selector', 'service-time 0')], ])

# same results as all_devs_conf
ugly2_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/ugly2.conf'), None, None, None, None, None, None, False, False, False, True,
     True, False, False, True, True,
     [('path_checker', 'rdac'), ('detect_checker', 'yes'), ('features', '2 pg_init_retries 50'),
      ('path_selector', 'service-time 0'), ('fast_io_fail_tmo', '5'), ('no_path_retry', 'queue'), ], ])

just_checker_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/just_checker.conf'), 'rdac', '/etc/multipath/conf.d', True, True, None, False,
     False, False, False, False, False, False, False, False, False, [], ])

just_detect_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/just_detect.conf'), None, None, None, False, None, None, False, False, False,
     False, False, False, False, False, False, [], ])

just_reassign_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/just_reassign.conf'), None, None, None, None, None, True, False, False, False,
     False, False, False, False, False, False, [], ])

just_exists_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/just_exists.conf'), None, None, None, None, None, None, False, False, False,
     False, False, False, False, True, False, [], ])

just_all_devs_conf = build_config(
    [os.path.join(CUR_DIR, 'files/before/just_all_devs.conf'), None, None, None, None, None, None, False, False, False,
     False, False, False, False, False, True, [], ])


def test_configs():
    tests = [(default_rhel7_conf, os.path.join(CUR_DIR, 'files/after/default_rhel7.conf'),),
             (all_devs_conf, os.path.join(CUR_DIR, 'files/after/all_devs.conf')), (empty_conf, None),
             (default_rhel8_conf, None),
             (all_the_things_conf, os.path.join(CUR_DIR, 'files/after/all_the_things.conf'),),
             (already_updated_conf, None), (ugly1_conf, os.path.join(CUR_DIR, 'files/after/ugly1.conf')),
             (ugly2_conf, os.path.join(CUR_DIR, 'files/after/ugly2.conf')),
             (just_checker_conf, os.path.join(CUR_DIR, 'files/after/just_checker.conf'),),
             (just_detect_conf, os.path.join(CUR_DIR, 'files/after/just_detect.conf'),),
             (just_reassign_conf, os.path.join(CUR_DIR, 'files/after/just_reassign.conf'),),
             (just_exists_conf, os.path.join(CUR_DIR, 'files/after/just_exists.conf'),),
             (just_all_devs_conf, os.path.join(CUR_DIR, 'files/after/just_all_devs.conf'),), ]
    for config, expected_config in tests:
        config_lines = multipathconfupdate._update_config(config)
        if config_lines is None:
            assert expected_config is None
            continue
        expected_lines = multipathutil.read_config(expected_config)
        assert expected_lines is not None
        assert len(config_lines) == len(expected_lines)
        for config_line, expected_line in zip(config_lines, expected_lines):
            assert config_line == expected_line


# repos/system_upgrade/el7toel8/actors/networkmanagerreadconfig/libraries/networkmanagerreadconfig.py

from six.moves.configparser import ParsingError

from leapp.libraries.common import utils
from leapp.libraries.stdlib import CalledProcessError, run, api


def read_nm_config(file_path=None):
    if file_path:
        try:
            with open(file_path, 'r') as f:
                r = f.read()
                return r
        except IOError as e:
            api.current_logger().warning('Error reading NetworkManager configuration from {}: {}'.format(file_path, e))
            return None
    else:
        try:
            # Use 'NM --print-config' to read the configurationo so
            # that the main configuration file and other files in
            # various directories get merged in the right way.
            r = run(['NetworkManager', '--print-config'], split=False)['stdout']
            return r
        except (OSError, CalledProcessError) as e:
            api.current_logger().warning('Error reading NetworkManager configuration: {}'.format(e))
            return None


def parse_nm_config(cfg):
    try:
        return utils.parse_config(cfg)
    except (ParsingError, TypeError) as e:
        api.current_logger().warning('Error parsing NetworkManager configuration: {}'.format(e))
        return None


def check_nm_dhcp(nm_cfg, parser):
    if parser.has_option('main', 'dhcp'):
        nm_cfg.dhcp = parser.get("main", "dhcp")


# repos/system_upgrade/el7toel8/actors/networkmanagerreadconfig/tests/unit_test_networkmanagerreadconfig.py

import os

from leapp.libraries.actor import networkmanagerreadconfig

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


def test_nm_with_dhcp():
    config = networkmanagerreadconfig.read_nm_config(file_path=os.path.join(CUR_DIR, 'files/nm_cfg_with_dhcp'))
    parser = networkmanagerreadconfig.parse_nm_config(config)

    assert config
    assert parser
    assert parser.has_option('main', 'dhcp')


def test_nm_without_dhcp():
    config = networkmanagerreadconfig.read_nm_config(file_path=os.path.join(CUR_DIR, 'files/nm_cfg_without_dhcp'))
    parser = networkmanagerreadconfig.parse_nm_config(config)

    assert config
    assert parser
    assert not parser.has_option('main', 'dhcp')


def test_nm_with_error():
    config = networkmanagerreadconfig.read_nm_config(file_path=os.path.join(CUR_DIR, 'files/nm_cfg_file_error'))
    parser = networkmanagerreadconfig.parse_nm_config(config)

    assert config
    assert parser
    assert not parser.has_section('main')


# repos/system_upgrade/el7toel8/actors/networkmanagerupdateconnections/tools/nm-update-client-ids.py

from __future__ import print_function

import sys

import gi
gi.require_version('NM', '1.0')
from gi.repository import NM  # noqa: E402; pylint: disable=wrong-import-position


def is_hexstring(s):
    arr = s.split(':')
    for a in arr:
        if len(a) != 1 and len(a) != 2:
            return False
        try:
            int(a, 16)
        except ValueError:
            return False
    return True


client = NM.Client.new(None)
if not client:
    print('Cannot create NM client instance')
    sys.exit(0)

processed = 0
changed = 0
errors = 0

for c in client.get_connections():
    s_ip4 = c.get_setting_ip4_config()
    processed += 1
    if s_ip4 is not None:
        client_id = s_ip4.get_dhcp_client_id()
        if client_id is not None:
            if not is_hexstring(client_id):
                new_client_id = ':'.join(hex(ord(x))[2:] for x in client_id)
                s_ip4.set_property(NM.SETTING_IP4_CONFIG_DHCP_CLIENT_ID, new_client_id)
                success = c.commit_changes(True, None)
                if success:
                    changed += 1
                else:
                    errors += 1
                print('Connection {}: \'{}\' -> \'{}\' ({})'.format(c.get_uuid(),
                                                                    client_id, new_client_id,
                                                                    'OK' if success else 'FAIL'))

print("{} processed, {} changed, {} errors".format(processed, changed, errors))


# repos/system_upgrade/el7toel8/actors/opensshalgorithmscheck/libraries/opensshalgorithmscheck.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp import reporting


def process(openssh_messages):
    removed_ciphers = [
        "blowfish-cbc",
        "cast128-cbc",
        "arcfour",
        "arcfour128",
        "arcfour256",
    ]
    removed_macs = [
        "hmac-ripemd160",
    ]
    found_ciphers = []
    found_macs = []

    config = next(openssh_messages, None)
    if list(openssh_messages):
        api.current_logger().warning('Unexpectedly received more than one OpenSshConfig message.')
    if not config:
        raise StopActorExecutionError(
            'Could not check openssh configuration', details={'details': 'No OpenSshConfig facts found.'}
        )

    for cipher in removed_ciphers:
        if config.ciphers and cipher in config.ciphers:
            found_ciphers.append(cipher)
    for mac in removed_macs:
        if config.macs and mac in config.macs:
            found_macs.append(mac)

    resources = [
        reporting.RelatedResource('package', 'openssh-server'),
        reporting.RelatedResource('file', '/etc/ssh/sshd_config')
    ]
    if found_ciphers:
        reporting.create_report([
            reporting.Title('OpenSSH configured to use removed ciphers'),
            reporting.Summary(
                'OpenSSH is configured to use removed ciphers {}. '
                'These ciphers were removed from OpenSSH and if '
                'present the sshd daemon will not start in RHEL 8'
                ''.format(','.join(found_ciphers))
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.NETWORK,
                    reporting.Tags.SERVICES
            ]),
            reporting.Remediation(
                hint='Remove the following ciphers from sshd_config: '
                     '{}'.format(','.join(found_ciphers))
            ),
            reporting.Flags([reporting.Flags.INHIBITOR])
        ] + resources)

    if found_macs:
        reporting.create_report([
            reporting.Title('OpenSSH configured to use removed mac'),
            reporting.Summary(
                'OpenSSH is configured to use removed mac {}. '
                'This MAC was removed from OpenSSH and if present '
                'the sshd daemon will not start in RHEL 8'
                ''.format(','.join(found_macs))
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.NETWORK,
                    reporting.Tags.SERVICES
            ]),
            reporting.Remediation(
                hint='Remove the following MACs from sshd_config: {}'.format(','.join(found_macs))
            ),
            reporting.Flags([reporting.Flags.INHIBITOR])
        ] + resources)


# repos/system_upgrade/el7toel8/actors/opensshalgorithmscheck/tests/unit_test_opensshalgorithmscheck.py

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import opensshalgorithmscheck
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin, Report
from leapp.snactor.fixture import current_actor_context


def test_no_config(current_actor_context):
    with pytest.raises(StopActorExecutionError):
        opensshalgorithmscheck.process(iter([]))


osprl = OpenSshPermitRootLogin(value='no')


@pytest.mark.parametrize('ciphers,expected_report', [
    (None, False),
    ('aes128-ctr', False),
    ('aes128-ctr,aes192-ctr,aes256-ctr', False),
    ('arcfour', True),
    ('arcfour,arcfour128,arcfour256', True),
    ('arcfour,aes128-ctr', True),
    ('aes128-ctr,arcfour', True)])
def test_ciphers(current_actor_context, ciphers, expected_report):
    current_actor_context.feed(OpenSshConfig(
        permit_root_login=[osprl],
        deprecated_directives=[],
        ciphers=ciphers
    ))
    current_actor_context.run()
    if expected_report:
        assert current_actor_context.consume(Report)
    else:
        assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/opensshconfigscanner/libraries/readopensshconfig.py

import errno

from leapp.models import OpenSshConfig, OpenSshPermitRootLogin
from leapp.libraries.stdlib import run, api


CONFIG = '/etc/ssh/sshd_config'
DEPRECATED_DIRECTIVES = ['showpatchlevel']


def line_empty(line):
    return len(line) == 0 or line.startswith('\n') or line.startswith('#')


def parse_config(config):
    """Parse OpenSSH server configuration or the output of sshd test option."""

    # RHEL7 defaults
    ret = OpenSshConfig(
        permit_root_login=[],
        deprecated_directives=[]
    )

    in_match = None
    for line in config:
        line = line.strip()
        if line_empty(line):
            continue

        el = line.split()
        if len(el) < 2:
            continue
        value = el[1]
        if el[0].lower() == 'match':
            in_match = el[1:]
            continue

        if el[0].lower() == 'permitrootlogin':
            # convert deprecated alias
            if value == "without-password":
                value = "prohibit-password"
            v = OpenSshPermitRootLogin(value=value, in_match=in_match)
            ret.permit_root_login.append(v)

        elif el[0].lower() == 'useprivilegeseparation':
            # Record only first occurence, which is effective
            if not ret.use_privilege_separation:
                ret.use_privilege_separation = value

        elif el[0].lower() == 'protocol':
            # Record only first occurence, which is effective
            if not ret.protocol:
                ret.protocol = value

        elif el[0].lower() == 'ciphers':
            # Record only first occurence, which is effective
            if not ret.ciphers:
                ret.ciphers = value

        elif el[0].lower() == 'macs':
            # Record only first occurence, which is effective
            if not ret.macs:
                ret.macs = value

        elif el[0].lower() in DEPRECATED_DIRECTIVES:
            # Filter out duplicit occurences of the same deprecated directive
            if el[0].lower() not in ret.deprecated_directives:
                # Use the directive in the form as found in config for user convenience
                ret.deprecated_directives.append(el[0])
    return ret


def produce_config(producer, config):
    """Produce a Leapp message with all interesting OpenSSH configuration options."""

    producer(config)


def read_sshd_config():
    """Read the actual sshd configuration file."""
    try:
        with open(CONFIG, 'r') as fd:
            return fd.readlines()
    except IOError as err:
        if err.errno != errno.ENOENT:
            error = 'Failed to read sshd_config: {}'.format(str(err))
            api.current_logger().error(error)
        return []


def read_rpm_modifications():
    """Asks RPM database whether the configuration file was modified."""

    try:
        return run(['rpm', '-Vf', CONFIG], split=True, checked=False)['stdout']
    except OSError as err:
        error = 'Failed to check the modification status of the {}: {}' \
                ''.format(CONFIG, str(err))
        api.current_logger().error(error)
        return []


def parse_config_modification(data):
    """Handle the output of rpm verify to figure out configuration file was modified."""

    # First assume it is not modified -- empty data says it is not modified
    modified = False
    for line in data:
        parts = line.split(' ')
        # The last part of the line is the actual file we care for
        if parts[-1] == CONFIG:
            # First part contains information, if the size and digest differ
            if '5' in parts[0] or 'S' in parts[0]:
                modified = True
        # Ignore any other files lurking here

    return modified


def scan_sshd(producer):
    """Parse sshd_config configuration file to create OpenSshConfig message."""

    # direct access to configuration file
    output = read_sshd_config()
    config = parse_config(output)

    # find out whether the file was modified from the one shipped in rpm
    output = read_rpm_modifications()
    config.modified = parse_config_modification(output)

    produce_config(producer, config)


# repos/system_upgrade/el7toel8/actors/opensshconfigscanner/tests/test_readopensshconfig_opensshconfigscanner.py

from leapp.models import OpenSshConfig, OpenSshPermitRootLogin
from leapp.libraries.actor.readopensshconfig import parse_config, \
    produce_config, line_empty, parse_config_modification


def test_line_empty():
    assert line_empty("".strip()) is True
    assert line_empty("     ".strip()) is True
    assert line_empty("   # comment".strip()) is True
    assert line_empty("# comment".strip()) is True
    assert line_empty("option".strip()) is False
    assert line_empty("    option".strip()) is False


def test_parse_config():
    config = [
        "# comment from file",
        "",  # empty line
        "   ",  # whitespace line
        "permitrootlogin prohibit-password",
        "permittty yes",
        "useprivilegeseparation no",
        "protocol 2",
        "hostkey /etc/ssh/ssh_host_ecdsa_key",  # unrelated duplicate keys
        "hostkey /etc/ssh/ssh_host_ed25519_key",
        "ciphers aes128-ctr",
        "macs hmac-md5",
    ]

    output = parse_config(config)
    assert isinstance(output, OpenSshConfig)
    assert len(output.permit_root_login) == 1
    assert output.permit_root_login[0].value == "prohibit-password"
    assert output.use_privilege_separation == "no"
    assert output.protocol == "2"
    assert output.ciphers == "aes128-ctr"
    assert output.macs == "hmac-md5"


def test_parse_config_case():
    config = [
        "PermitRootLogin prohibit-password",
        "UsePrivilegeSeparation yes",
        "Protocol 1",
    ]

    output = parse_config(config)
    assert isinstance(output, OpenSshConfig)
    assert len(output.permit_root_login) == 1
    assert output.permit_root_login[0].value == "prohibit-password"
    assert output.use_privilege_separation == "yes"
    assert output.protocol == "1"


def test_parse_config_multiple():
    config = [
        "PermitRootLogin prohibit-password",
        "PermitRootLogin no",
        "PermitRootLogin yes",
        "Ciphers aes128-cbc",
        "Ciphers aes256-cbc",
    ]

    output = parse_config(config)
    assert isinstance(output, OpenSshConfig)
    assert len(output.permit_root_login) == 3
    assert output.permit_root_login[0].value == "prohibit-password"
    assert output.permit_root_login[1].value == "no"
    assert output.permit_root_login[2].value == "yes"
    assert output.use_privilege_separation is None
    assert output.protocol is None
    assert output.ciphers == 'aes128-cbc'


def test_parse_config_commented():
    config = [
        "#PermitRootLogin no",
        "#UsePrivilegeSeparation no",
        "#Protocol 12",
    ]

    output = parse_config(config)
    assert isinstance(output, OpenSshConfig)
    assert not output.permit_root_login
    assert output.use_privilege_separation is None
    assert output.protocol is None


def test_parse_config_missing_argument():
    config = [
        "PermitRootLogin",
        "UsePrivilegeSeparation",
        "Protocol"
    ]

    output = parse_config(config)
    assert isinstance(output, OpenSshConfig)
    assert not output.permit_root_login
    assert output.use_privilege_separation is None
    assert output.protocol is None


def test_parse_config_match():
    config = [
        "PermitRootLogin yes",
        "Match address 192.168.*",
        "   PermitRootLogin no"
    ]

    output = parse_config(config)
    assert isinstance(output, OpenSshConfig)
    assert len(output.permit_root_login) == 2
    assert output.permit_root_login[0].value == 'yes'
    assert output.permit_root_login[0].in_match is None
    assert output.permit_root_login[1].value == 'no'
    assert output.permit_root_login[1].in_match == ['address', '192.168.*']
    assert output.use_privilege_separation is None
    assert output.protocol is None


def test_parse_config_deprecated():
    config = [
        "permitrootlogin without-password"
    ]
    output = parse_config(config)
    assert isinstance(output, OpenSshConfig)
    assert len(output.permit_root_login) == 1
    assert output.permit_root_login[0].value == "prohibit-password"


def test_parse_config_empty():
    output = parse_config([])
    assert isinstance(output, OpenSshConfig)
    assert isinstance(output, OpenSshConfig)
    assert not output.permit_root_login
    assert output.use_privilege_separation is None
    assert output.protocol is None


def test_parse_config_modification():
    # This one was modified
    data = [
        "S.5....T.  c /etc/ssh/sshd_config",
    ]
    assert parse_config_modification(data)

    # This one was just touched (timestamp does not match)
    data = [
        ".......T.  c /etc/ssh/sshd_config",
    ]
    assert not parse_config_modification(data)

    # This one was not modified (not listed at all)
    data = [
        ".......T.  c /etc/sysconfig/sshd",
    ]
    assert not parse_config_modification(data)

    # Parse multiple lines
    data = [
        "S.5....T.  c /etc/sysconfig/sshd",
        "S.5....T.  c /etc/ssh/sshd_config",
    ]
    assert parse_config_modification(data)


def test_produce_config():
    output = []

    def fake_producer(*args):
        output.extend(args)

    config = OpenSshConfig(
        permit_root_login=[OpenSshPermitRootLogin(value="no")],
        use_privilege_separation="yes",
        protocol="1",
        deprecated_directives=[],
    )

    produce_config(fake_producer, config)
    assert len(output) == 1
    cfg = output[0]
    assert len(cfg.permit_root_login) == 1
    assert cfg.permit_root_login[0].value == "no"
    assert cfg.use_privilege_separation == "yes"
    assert cfg.protocol == '1'


def test_actor_execution(current_actor_context):
    current_actor_context.run()
    assert current_actor_context.consume(OpenSshConfig)


# repos/system_upgrade/el7toel8/actors/opensshdeprecateddirectivescheck/libraries/opensshdeprecateddirectivescheck.py

from leapp import reporting


LIST_SEPARATOR_FMT = '\n    - '


def inhibit_if_deprecated_directives_used(ssh_config_msg):
    """ Inhibits the upgrade if any deprecated directives were found in the sshd configuration. """

    if ssh_config_msg.deprecated_directives:
        # Prepare the output of the deprecated directives for the user
        deprecated_directives_report_text = ''
        for deprecated_directive in ssh_config_msg.deprecated_directives:
            deprecated_directives_report_text += '{0}{1}'.format(LIST_SEPARATOR_FMT, deprecated_directive)

        sshd_config_path = '/etc/ssh/sshd_config'
        reporting.create_report([
            reporting.Title('A deprecated directive in the sshd configuration'),
            reporting.Summary(
                'The following deprecated directives were found in the sshd configuration file {0}:{1}'
                .format(sshd_config_path, deprecated_directives_report_text)
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SERVICES]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.RelatedResource('file', sshd_config_path),
            reporting.Remediation(
                hint='Remove the deprecated directives from the sshd configuration.',
                commands=[["sed", "-i", "/^\\s*ShowPatchLevel.*$/Id", sshd_config_path]]
            )
        ])


# repos/system_upgrade/el7toel8/actors/opensshdeprecateddirectivescheck/tests/test_opensshdeprecateddirectivescheck.py

import pytest

from leapp import reporting
from leapp.libraries.common.testutils import create_report_mocked, logger_mocked
from leapp.libraries.actor.opensshdeprecateddirectivescheck import inhibit_if_deprecated_directives_used
from leapp.models import OpenSshConfig


def test_inhibit_if_deprecated_directives_used(monkeypatch):
    """Tests whether the upgrade is inhibited when deprecated directives are used in config."""
    created_report = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', created_report)

    ssh_config = OpenSshConfig(
        permit_root_login=[],
        deprecated_directives=['ShowPatchLevel']
    )

    inhibit_if_deprecated_directives_used(ssh_config)

    fail_description = 'Report entry was not created when deprecated directive found in the ssh config.'
    assert created_report.called == 1, fail_description

    fail_description = 'Report doesn\'t have information about deprecated directive in the title.'
    assert 'deprecated directive' in created_report.report_fields['title'].lower(), fail_description

    fail_description = 'Report doesn\'t contain the (mocked) deprecated directive present in the config.'
    # The report should have the directive in a preserved form (same as found in configuration)
    assert 'ShowPatchLevel' in created_report.report_fields['summary'], fail_description

    assert created_report.report_fields['severity'] == 'high', 'Report has incorrect severity.'

    fail_description = 'Report should have the inhibition flag set when deprecated directive is present.'
    assert 'inhibitor' in created_report.report_fields['flags'], fail_description

    assert created_report.report_fields['remediations'], 'Report should carry some remediation information.'


def test_inhibit_if_deprecated_directives_used_no_deprecated_directives(monkeypatch):
    """Tests whether the upgrade is not inhibited when no deprecated directives are used in config."""
    created_report = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', created_report)

    ssh_config = OpenSshConfig(
        permit_root_login=[],
        deprecated_directives=[]
    )

    inhibit_if_deprecated_directives_used(ssh_config)
    assert created_report.called == 0, 'No report should be created if no deprecated directive present in the config.'


# repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/libraries/opensshpermitrootlogincheck.py



def semantics_changes(config):
    globally_enabled = False
    in_match_disabled = False
    for opt in config.permit_root_login:
        if opt.value != "yes" and opt.in_match is not None \
                and opt.in_match[0].lower() != 'all':
            in_match_disabled = True

        if opt.value == "yes" and (opt.in_match is None or
                                   opt.in_match[0].lower() == 'all'):
            globally_enabled = True

    return not globally_enabled and in_match_disabled


# repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/tests/test_library_opensshpermitrootlogincheck.py

from leapp.libraries.actor.opensshpermitrootlogincheck import semantics_changes
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin


def test_globally_enabled():
    """ Configuration file in this format:

        PermitRootLogin yes # explicit
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='yes',
                in_match=None)
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_globally_disabled():
    """ Configuration file in this format:

        PermitRootLogin no # explicit
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='no',
                in_match=None)
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_globally_disabled_password():
    """ Configuration file in this format:

        PermitRootLogin prohibit-password # explicit
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='prohibit-password',
                in_match=None)
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_in_match_disabled():
    """ Configuration file in this format:

        # PermitRootLogin yes # implicit
        Match address 10.10.*
            PermitRootLogin no
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='no',
                in_match=['address', '10.10.*'])
        ],
        deprecated_directives=[]
    )

    assert semantics_changes(config)


def test_in_match_disabled_password():
    """ Configuration file in this format:

        # PermitRootLogin yes # implicit
        Match address 192.168.*
            PermitRootLogin prohibit-password
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='prohibit-password',
                in_match=['address', '10.10.*'])
        ],
        deprecated_directives=[]
    )

    assert semantics_changes(config)


def test_in_match_enabled():
    """ Configuration file in this format:

        # PermitRootLogin yes # implicit
        Match address 192.168.*
            PermitRootLogin yes
    """
    # TODO This is suspicious configuration we should probably handle separately
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='yes',
                in_match=['address', '192.168.*'])
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_in_match_all_disabled():
    """ Configuration file in this format:

        # PermitRootLogin yes # implicit
        Match all
            PermitRootLogin no
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='no',
                in_match=['all'])
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_in_match_all_disabled_password():
    """ Configuration file in this format:

        # PermitRootLogin yes # implicit
        Match all
            PermitRootLogin prohibit-password
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='prohibit-password',
                in_match=['all'])
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_in_match_all_enabled():
    """ Configuration file in this format:

        # PermitRootLogin yes # implicit
        Match all
            PermitRootLogin yes
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='yes',
                in_match=['all'])
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_in_match_enabled_globally_disabled():
    """ Configuration file in this format:

        PermitRootLogin no # explicit
        Match address 192.*
            PermitRootLogin yes
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='no',
                in_match=None),
            OpenSshPermitRootLogin(
                value='yes',
                in_match=['address', '192.*'])
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


def test_in_match_disabled_globally_enabled():
    """ Configuration file in this format:

        PermitRootLogin yes # explicit
        Match address 192.*
            PermitRootLogin no
    """
    config = OpenSshConfig(
        permit_root_login=[
            OpenSshPermitRootLogin(
                value='yes',
                in_match=None),
            OpenSshPermitRootLogin(
                value='no',
                in_match=['address', '192.*'])
        ],
        deprecated_directives=[]
    )

    assert not semantics_changes(config)


# repos/system_upgrade/el7toel8/actors/opensshprotocolcheck/libraries/opensshprotocolcheck.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp import reporting


def process(openssh_messages):
    config = next(openssh_messages, None)
    if list(openssh_messages):
        api.current_logger().warning('Unexpectedly received more than one OpenSshConfig message.')
    if not config:
        raise StopActorExecutionError(
            'Could not check openssh configuration', details={'details': 'No OpenSshConfig facts found.'}
        )

    if config.protocol:
        reporting.create_report([
            reporting.Title('OpenSSH configured with removed configuration Protocol'),
            reporting.Summary(
                'OpenSSH is configured with removed configuration '
                'option Protocol. If this used to be for enabling '
                'SSHv1, this is no longer supported in RHEL 8. '
                'Otherwise this option can be simply removed.'
            ),
            reporting.Severity(reporting.Severity.LOW),
            reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.NETWORK,
                    reporting.Tags.SERVICES
            ]),
        ])


# repos/system_upgrade/el7toel8/actors/opensshprotocolcheck/tests/unit_test_opensshprotocolcheck.py

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import opensshprotocolcheck
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin, Report
from leapp.snactor.fixture import current_actor_context


def test_no_config(current_actor_context):
    with pytest.raises(StopActorExecutionError):
        opensshprotocolcheck.process(iter([]))


osprl = OpenSshPermitRootLogin(value='no')


@pytest.mark.parametrize('protocol', [None, '1', '2', '1,2', '2,1', '7'])
def test_protocol(current_actor_context, protocol):
    current_actor_context.feed(OpenSshConfig(
        permit_root_login=[osprl],
        protocol=protocol,
        deprecated_directives=[]
    ))
    current_actor_context.run()
    if protocol:
        assert current_actor_context.consume(Report)
    else:
        assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/opensshuseprivilegeseparationcheck/libraries/opensshuseprivilegeseparationcheck.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp import reporting


def process(openssh_messages):
    config = next(openssh_messages, None)
    if list(openssh_messages):
        api.current_logger().warning('Unexpectedly received more than one OpenSshConfig message.')
    if not config:
        raise StopActorExecutionError(
            'Could not check openssh configuration', details={'details': 'No OpenSshConfig facts found.'}
        )

    if config.use_privilege_separation is not None and \
       config.use_privilege_separation != "sandbox":
        reporting.create_report([
            reporting.Title('OpenSSH configured not to use privilege separation sandbox'),
            reporting.Summary(
                'OpenSSH is configured to disable privilege '
                'separation sandbox, which is decreasing security '
                'and is no longer supported in RHEL 8'
            ),
            reporting.Severity(reporting.Severity.LOW),
            reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.NETWORK,
                    reporting.Tags.SERVICES
            ]),
        ])


# repos/system_upgrade/el7toel8/actors/opensshuseprivilegeseparationcheck/tests/unit_test_opensshuseprivilegeseparationcheck.py

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import opensshuseprivilegeseparationcheck
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin, Report
from leapp.snactor.fixture import current_actor_context


def test_no_config(current_actor_context):
    with pytest.raises(StopActorExecutionError):
        opensshuseprivilegeseparationcheck.process(iter([]))


osprl = OpenSshPermitRootLogin(value='no')


@pytest.mark.parametrize('values,expected_report', [
    ([''], False),
    (['sandbox'], False),
    (['yes'], True),
    (['no'], True),
    (['sandbox', 'yes'], False),
    (['yes', 'sandbox'], True)])
def test_separation(current_actor_context, values, expected_report):
    for value in values:
        if value:
            current_actor_context.feed(OpenSshConfig(
                permit_root_login=[osprl],
                use_privilege_separation=value,
                deprecated_directives=[]
            ))
        else:
            current_actor_context.feed(OpenSshConfig(
                permit_root_login=[osprl],
                deprecated_directives=[]
            ))
    current_actor_context.run()
    if expected_report:
        assert current_actor_context.consume(Report)
    else:
        assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/postgresqlcheck/libraries/postgresqlcheck.py

from leapp import reporting
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM


# Summary for postgresql-server report
report_server_inst_summary = (
    'PostgreSQL server component will be upgraded. Since RHEL-8 includes'
    ' PostgreSQL server 10 by default, which is incompatible with 9.2'
    ' included in RHEL-7, it is necessary to proceed with additional steps'
    ' for the complete upgrade of the PostgreSQL data.'
)

report_server_inst_hint = (
    'Back up your data before proceeding with the upgrade'
    ' and follow steps in the documentation section "Migrating to a RHEL 8 version of PostgreSQL"'
    ' after the upgrade.'
)

# Link URL for postgresql-server report
report_server_inst_link_url = 'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/deploying_different_types_of_servers/index#migrating-to-a-rhel-8-version-of-postgresql_using-postgresql'  # noqa: E501; pylint: disable=line-too-long

# List of dropped extensions from postgresql-contrib package
report_contrib_inst_dropext = ['dummy_seclabel', 'test_parser', 'tsearch2']

# Summary for postgresql-contrib report
report_contrib_inst_summary = (
    'Please note that some extensions have been dropped from the'
    ' postgresql-contrib package and might not be available after'
    ' the upgrade:{}'
    .format(''.join(['\n    - {}'.format(i) for i in report_contrib_inst_dropext]))
)


def _report_server_installed():
    """
    Create report on postgresql-server package installation detection.

    Should remind user about present PostgreSQL server package
    installation, warn them about necessary additional steps, and
    redirect them to online documentation for the upgrade process.
    """
    reporting.create_report([
        reporting.Title('PostgreSQL (postgresql-server) has been detected on your system'),
        reporting.Summary(report_server_inst_summary),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Tags([reporting.Tags.SERVICES]),
        reporting.ExternalLink(title='Migrating to a RHEL 8 version of PostgreSQL',
                               url=report_server_inst_link_url),
        reporting.RelatedResource('package', 'postgresql-server'),
        reporting.Remediation(hint=report_server_inst_hint),
        ])


def _report_contrib_installed():
    """
    Create report on postgresql-contrib package installation detection.

    Should remind user about present PostgreSQL contrib package
    installation and provide them with a list of extensions no longer
    shipped with this package.
    """
    reporting.create_report([
        reporting.Title('PostgreSQL (postgresql-contrib) has been detected on your system'),
        reporting.Summary(report_contrib_inst_summary),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Tags([reporting.Tags.SERVICES]),
        reporting.RelatedResource('package', 'postgresql-contrib')
        ])


def report_installed_packages(_context=api):
    """
    Create reports according to detected PostgreSQL packages.

    Create the report if the postgresql-server rpm (RH signed) is installed.
    Additionally, create another report if the postgresql-contrib rpm
    is installed.
    """
    has_server = has_package(InstalledRedHatSignedRPM, 'postgresql-server', context=_context)
    has_contrib = has_package(InstalledRedHatSignedRPM, 'postgresql-contrib', context=_context)

    if has_server:
        # postgresql-server
        _report_server_installed()
        if has_contrib:
            # postgresql-contrib
            _report_contrib_installed()


# repos/system_upgrade/el7toel8/actors/postgresqlcheck/tests/test_postgresqlcheck.py

import pytest

from leapp import reporting
from leapp.libraries.actor.postgresqlcheck import report_installed_packages
from leapp.libraries.common.testutils import CurrentActorMocked, create_report_mocked
from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM, RPM


def _generate_rpm_with_name(name):
    """
    Generate new RPM model item with given name.

    Parameters:
        name (str): rpm name

    Returns:
        rpm  (RPM): new RPM object with name parameter set
    """
    return RPM(name=name,
               version='0.1',
               release='1.sm01',
               epoch='1',
               pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51',
               packager='Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>',
               arch='noarch')


@pytest.mark.parametrize('has_server,has_contrib', [
    (True, True),  # both server, contrib
    (True, False),  # only server
    (False, False),  # neither
])
def test_actor_execution(monkeypatch, has_server, has_contrib):
    """
    Parametrized helper function for test_actor_* functions.

    First generate list of RPM models based on set arguments. Then, run
    the actor feeded with our RPM list. Finally, assert Reports
    according to set arguments.

    Parameters:
        has_server  (bool): postgresql-server installed
        has_contrib (bool): postgresql-contrib installed
    """

    # Couple of random packages
    rpms = [_generate_rpm_with_name('sed'),
            _generate_rpm_with_name('htop')]

    if has_server:
        # Add postgresql-server
        rpms += [_generate_rpm_with_name('postgresql-server')]
        if has_contrib:
            # Add postgresql-contrib
            rpms += [_generate_rpm_with_name('postgresql-contrib')]

    curr_actor_mocked = CurrentActorMocked(msgs=[InstalledRedHatSignedRPM(items=rpms)])
    monkeypatch.setattr(api, 'current_actor', curr_actor_mocked)
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    # Executed actor feeded with out fake RPMs
    report_installed_packages(_context=api)

    if has_server and has_contrib:
        # Assert for postgresql-server and postgresql-contrib packages installed
        assert reporting.create_report.called == 2
    elif has_server:
        # Assert only for postgresql-server package installed
        assert reporting.create_report.called == 1
    else:
        # Assert for no postgresql packages installed
        assert not reporting.create_report.called


# repos/system_upgrade/el7toel8/actors/powertop/tests/component_test_powertop.py

from leapp.snactor.fixture import current_actor_context
from leapp.models import InstalledRedHatSignedRPM, RPM
from leapp.reporting import Report

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'


def create_modulesfacts(installed_rpm):
    return InstalledRedHatSignedRPM(items=installed_rpm)


def test_actor_with_powertop_package(current_actor_context):
    with_powertop = [
        RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='powertop', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=with_powertop))
    current_actor_context.run()
    assert current_actor_context.consume(Report)


def test_actor_without_powertop_package(current_actor_context):
    without_powertop = [
        RPM(name='grep', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51'),
        RPM(name='sed', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 199e2f91fd431d51')]

    current_actor_context.feed(create_modulesfacts(installed_rpm=without_powertop))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/prepareyumconfig/tests/unit_test_prepareyumconfig.py

import os

import pytest

from leapp.libraries.common.utils import api, apply_yum_workaround, mounting

CUR_DIR = os.path.dirname(os.path.abspath(__file__))


@pytest.fixture
def adjust_cwd():
    previous_cwd = os.getcwd()
    os.chdir(os.path.join(CUR_DIR, "../"))
    yield
    os.chdir(previous_cwd)


class MockedNotIsolatedActions(object):
    def __init__(self):
        self.called = 0
        self.args = None

    def call(self, args):
        self.called += 1
        self.args = args
        return {'stdout': ''}

    def __call__(self, *args, **kwargs):
        return self


def _get_tool_path(name):
    for directory in os.getenv('LEAPP_COMMON_TOOLS', '').split(':'):
        full_path = os.path.join(directory, name)
        if os.path.isfile(full_path):
            return full_path
    return None


def test_prepare_yum_config(monkeypatch, adjust_cwd):
    actions = MockedNotIsolatedActions()
    monkeypatch.setattr(api, "get_tool_path", _get_tool_path)
    monkeypatch.setattr(mounting, "NotIsolatedActions", actions)
    apply_yum_workaround()
    assert actions.called == 1
    assert os.path.basename(actions.args[-1]) == 'handleyumconfig'


# repos/system_upgrade/el7toel8/actors/quaggadaemons/libraries/quaggadaemons.py

from leapp.libraries.stdlib import CalledProcessError, api, run
from leapp.models import QuaggaToFrrFacts

QUAGGA_DAEMONS = [
    'babeld',
    'bgpd',
    'isisd',
    'ospf6d',
    'ospfd',
    'ripd',
    'ripngd',
    'zebra'
]


def _check_service(name, state):
    try:
        run(['systemctl', 'is-{}'.format(state), name])
        api.current_logger().debug('%s is %s', name, state)
    except CalledProcessError:
        api.current_logger().debug('%s is not %s', name, state)
        return False

    return True


def process_daemons():
    active_daemons = [daemon for daemon in QUAGGA_DAEMONS if _check_service(daemon, 'active')]
    enabled_daemons = [daemon for daemon in QUAGGA_DAEMONS if _check_service(daemon, 'enabled')]

    if active_daemons:
        api.current_logger().debug('active quaggadaemons: %s', ', '.join(active_daemons))

    if enabled_daemons:
        api.current_logger().debug('enabled quaggadaemons: %s', ', '.join(enabled_daemons))

    return QuaggaToFrrFacts(active_daemons=active_daemons, enabled_daemons=enabled_daemons)


# repos/system_upgrade/el7toel8/actors/quaggadaemons/tests/test_unit_quaggadaemons.py

from leapp.libraries.actor import quaggadaemons
from leapp.models import QuaggaToFrrFacts

# daemons for mocked _check_service function
TEST_DAEMONS = ['bgpd', 'ospfd', 'zebra']


def mock_check_service(name, state):
    if name in TEST_DAEMONS:
        return True

    return False


def test_process_daemons():
    quaggadaemons._check_service = mock_check_service

    facts = quaggadaemons.process_daemons()
    assert isinstance(facts, QuaggaToFrrFacts)
    assert facts.active_daemons == TEST_DAEMONS
    assert facts.enabled_daemons == TEST_DAEMONS


# repos/system_upgrade/el7toel8/actors/quaggareport/tests/test_quaggareport.py

import pytest

from leapp.models import QuaggaToFrrFacts
from leapp.snactor.fixture import ActorContext


# TODO We can't use caplog here as logs from other processes is
#  hard to capture and caplog not see it.
@pytest.mark.parametrize(
    ("quagga_facts", "active_daemons", "has_report", "msg_in_log"),
    [
        (True, ["babeld"], True, None),
        (True, ["something_else"], False, "babeld not used, moving on"),
        (False, [], False, None),
    ],
)
def test_quaggareport(
    monkeypatch,
    current_actor_context,
    quagga_facts,
    active_daemons,
    has_report,
    msg_in_log,
):
    """Test quaggareport.

    :type current_actor_context:ActorContext
    """
    if quagga_facts:
        current_actor_context.feed(
            QuaggaToFrrFacts(
                active_daemons=active_daemons,
                enabled_daemons=["bgpd", "ospfd", "zebra"],
            )
        )
    current_actor_context.run()
    if has_report:
        assert current_actor_context.messages()[0]["type"] == "Report"
    if msg_in_log:
        assert not current_actor_context.messages()


# repos/system_upgrade/el7toel8/actors/quaggatofrr/libraries/quaggatofrr.py

import os
import re
import shutil

from leapp.libraries.common.config import version
from leapp.libraries.stdlib import CalledProcessError, api, run

DAEMON_FILE = '/etc/frr/daemons'
# if this file sitll exists after the removal of quagga, it has been modified
CONFIG_FILE = '/etc/sysconfig/quagga.rpmsave'
QUAGGA_CONF_FILES = '/etc/quagga/'
FRR_CONF_FILES = '/etc/frr/'
BGPD_CONF_FILE = '/etc/frr/bgpd.conf'

regex = re.compile(r'\w+(?<!WATCH)(?<!BABELD)_OPTS=".*"')


def _get_config_data(path):
    conf_data = {}
    with open(path) as f:
        for line in f:
            if regex.match(line):
                k, v = line.rstrip().split("=")
                conf_data[k.split("_")[0].lower()] = v.strip('"')

    return conf_data


def _edit_new_config(path, active_daemons, config_data):
    with open(path, 'r') as f:
        data = f.read()

    # replace no as yes in /etc/frr/daemons
    for daemon in active_daemons:
        data = re.sub(r'{}=no'.format(daemon), r'{}=yes'.format(daemon), data, flags=re.MULTILINE)

    if config_data:
        for daemon in config_data:
            data = re.sub(r'{}_options=\(".*"\)'.format(daemon),
                          r'{}_options=("{}")'.format(daemon, config_data[daemon]),
                          data, flags=re.MULTILINE)

    return data


# 1. parse /etc/sysconfig/quagga.rpmsave if it exists
# 2. change =no to =yes with every enabled daemon
# 3. use data from data from quagga.rpmsave in new daemon file
def _change_config(quagga_facts):
    config_data = {}
    if os.path.isfile(CONFIG_FILE):
        config_data = _get_config_data(CONFIG_FILE)

    # This file should definitely exist, if not, something went wrong with the upgrade
    if os.path.isfile(DAEMON_FILE):
        data = _edit_new_config(DAEMON_FILE, quagga_facts.active_daemons, config_data)
        with open(DAEMON_FILE, 'w') as f:
            f.write(data)


# In quagga, each daemon needed to be started individually
# In frr, only frr is started as a daemon, the rest is started based on the daemons file
# So as long as at least one daemon was active in quagga, frr should be enabled
def _enable_frr(quagga_facts):
    # remove babeld?
    if quagga_facts.enabled_daemons:
        try:
            run(['systemctl', 'enable', 'frr'])
        except CalledProcessError:
            return False

    return True


# due to an error in quagga, the conf files are not deleted after uninstall
# we can copy them as they are
def _copy_config_files(src_path, dest_path):
    conf_files = os.listdir(src_path)
    for file_name in conf_files:
        full_path = os.path.join(src_path, file_name)
        if os.path.isfile(full_path):
            shutil.copy(full_path, dest_path)
            api.current_logger().debug('Copying %s to %s%s', full_path, dest_path, file_name)


# some commands in *.conf files have changed in the latest rebase
def _fix_commands():
    if version.matches_target_version(">= 8.4"):
        if os.path.isfile(BGPD_CONF_FILE):
            with open(BGPD_CONF_FILE, 'r') as f:
                data = f.read()
                data = re.sub("ip extcommunity-list", "bgp extcommunity-list", data, flags=re.MULTILINE)
            with open(BGPD_CONF_FILE, 'w') as f:
                f.write(data)


def process_facts(quagga_facts):
    _change_config(quagga_facts)
    _copy_config_files(QUAGGA_CONF_FILES, FRR_CONF_FILES)
    _fix_commands()
    _enable_frr(quagga_facts)


# repos/system_upgrade/el7toel8/actors/quaggatofrr/tests/test_unit_quaggatofrr.py

import contextlib
import os
import shutil

import pytest

from leapp.libraries.actor import quaggatofrr
from leapp.libraries.common.testutils import CurrentActorMocked

ACTIVE_DAEMONS = ['bgpd', 'ospfd', 'zebra']
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
FROM_DIR = '/tmp/from_dir/'
TO_DIR = '/tmp/to_dir/'
CONFIG_DATA = {
    'bgpd': '--daemon -A 10.10.100.1',
    'isisd': '--daemon -A ::1',
    'ospf6d': '-A ::1',
    'ospfd': '-A 127.0.0.1',
    'ripd': '-A 127.0.0.1',
    'ripngd': '-A ::1',
    'zebra': '-s 90000000 --daemon -A 127.0.0.1'
}


@contextlib.contextmanager
def _create_mock_files():
    try:
        os.mkdir(FROM_DIR)
        os.mkdir(TO_DIR)

        for num in range(1, 10):
            full_path = "{}test_file_{}".format(FROM_DIR, num)
            with open(full_path, 'w') as fp:
                fp.write("test_file_{}".format(num))
        yield
    finally:
        shutil.rmtree(FROM_DIR)
        shutil.rmtree(TO_DIR)


class MockedFilePointer(object):
    def __init__(self, orig_open, fname, mode='r'):
        self._orig_open = orig_open
        self.fname = fname
        self.mode = mode
        # we want always read only..
        self._fp = self._orig_open(fname, 'r')
        self._read = None
        self.written = None

    def __enter__(self):
        return self

    def __exit__(self, *args, **kwargs):
        self.close()

    def close(self):
        if self._fp:
            self._fp.close()
            self._fp = None

    def read(self):
        self._read = self._fp.read()
        return self._read

    def write(self, data):
        if not self.written:
            self.written = data
        else:
            self.written += data


class MockedOpen(object):
    """
    This is mock for the open function. When called it creates
    the MockedFilePointer object.
    """

    def __init__(self):
        # currently we want to actually open the real files, we need
        # to mock other stuff related to file pointers / file objects
        self._orig_open = open
        self._open_called = []

    def __call__(self, fname, mode='r'):
        opened = MockedFilePointer(self._orig_open, fname, mode)
        self._open_called.append(opened)
        return opened

    def get_mocked_pointers(self, fname, mode=None):
        """
        Get list of MockedFilePointer objects with the specified fname.

        if the mode is set (expected 'r', 'rw', 'w' ..) discovered files are
        additionaly filtered to match the same mode (same string).
        """
        fnames = [i for i in self._open_called if i.fname == fname]
        return fnames if not mode else [i for i in fnames if i.mode == mode]


def test_copy_config_files():
    with _create_mock_files():
        quaggatofrr._copy_config_files(FROM_DIR, TO_DIR)
        conf_files = os.listdir(TO_DIR)
        for file_name in conf_files:
            full_path = os.path.join(TO_DIR, file_name)
            assert os.path.isfile(full_path)


def test_get_config_data():
    conf_data = quaggatofrr._get_config_data(
        os.path.join(CUR_DIR, 'files/quagga')
    )

    assert 'babels' not in conf_data
    assert conf_data['bgpd'] == CONFIG_DATA['bgpd']
    assert conf_data['isisd'] == CONFIG_DATA['isisd']
    assert conf_data['ospf6d'] == CONFIG_DATA['ospf6d']
    assert conf_data['ospfd'] == CONFIG_DATA['ospfd']
    assert conf_data['ripd'] == CONFIG_DATA['ripd']
    assert conf_data['ripngd'] == CONFIG_DATA['ripngd']
    assert conf_data['zebra'] == CONFIG_DATA['zebra']


def test_edit_new_config():
    # writing the data to the new config file
    data = quaggatofrr._edit_new_config(
        os.path.join(CUR_DIR, 'files/daemons'),
        ACTIVE_DAEMONS,
        CONFIG_DATA
    )

    assert 'zebra=yes' in data
    assert 'bgpd=yes' in data
    assert 'ospfd=yes' in data
    assert 'zebra_options=("-s 90000000 --daemon -A 127.0.0.1")' in data
    assert 'bgpd_options=("--daemon -A 10.10.100.1")' in data
    assert 'ospfd_options=("-A 127.0.0.1")' in data
    assert 'ospf6d_options=("-A ::1")' in data
    assert 'ripd_options=("-A 127.0.0.1")' in data
    assert 'ripngd_options=("-A ::1")' in data
    assert 'isisd_options=("--daemon -A ::1")' in data


@pytest.mark.parametrize('dst_ver', ['8.4', '8.5'])
def test_fix_commands(monkeypatch, dst_ver):
    monkeypatch.setattr(quaggatofrr, "BGPD_CONF_FILE", os.path.join(CUR_DIR, 'files/bgpd.conf'))
    monkeypatch.setattr(quaggatofrr.api, 'current_actor', CurrentActorMocked(dst_ver=dst_ver))
    monkeypatch.setattr(quaggatofrr, "open", MockedOpen(), False)
    quaggatofrr._fix_commands()

    fp_list = quaggatofrr.open.get_mocked_pointers(quaggatofrr.BGPD_CONF_FILE, "w")
    assert len(fp_list) == 1
    assert 'bgp extcommunity-list' in fp_list[0].written


def test_fix_commands_not_applied(monkeypatch):
    is_file_called = False

    def mocked_is_file(dummy):
        is_file_called = True
        return is_file_called

    monkeypatch.setattr(quaggatofrr.api, 'current_actor', CurrentActorMocked(dst_ver='8.3'))
    monkeypatch.setattr(os.path, 'isfile', mocked_is_file)
    monkeypatch.setattr(quaggatofrr, "open", MockedOpen(), False)
    quaggatofrr._fix_commands()
    assert not is_file_called


# repos/system_upgrade/el7toel8/actors/removeoldpammodulesapply/libraries/removeoldpammodulesapply.py

import os
import re


def read_file(config):
    """
        Read file contents. Return empty string if the file does not exist.
    """
    if not os.path.isfile(config):
        return ""
    with open(config) as f:
        return f.read()


def comment_modules(modules, content):
    """
    Disable modules in file content by commenting them.
    """
    for module in modules:
        content = re.sub(
            r'^([ \t]*[^#\s]+.*{0}\.so.*)$'.format(module),
            r'#\1',
            content,
            flags=re.MULTILINE
        )

    return content


# repos/system_upgrade/el7toel8/actors/removeoldpammodulesapply/tests/unit_test_removeoldpammodulesapply.py

import textwrap

from leapp.libraries.actor.removeoldpammodulesapply import comment_modules, read_file


def get_config(config):
    return textwrap.dedent(config).strip()


def test_read_file__non_existent():
    content = read_file('/this/does/not/exist')
    assert content == ''


def test_read_file__ok():
    content = read_file(__file__)
    assert content != ''
    assert 'test_read_file__ok' in content


def test_comment_modules__none():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_pkcs11.so
    auth sufficient pam_krb5.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')
    expected = pam

    content = comment_modules([], pam)
    assert content == expected


def test_comment_modules__replaced_single():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_pkcs11.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')
    expected = get_config('''
    auth sufficient pam_unix.so
    #auth sufficient pam_pkcs11.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    content = comment_modules(['pam_pkcs11', 'pam_krb5'], pam)
    assert content == expected


def test_comment_modules__replaced_all():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_pkcs11.so
    auth sufficient pam_krb5.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')
    expected = get_config('''
    auth sufficient pam_unix.so
    #auth sufficient pam_pkcs11.so
    #auth sufficient pam_krb5.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    content = comment_modules(['pam_pkcs11', 'pam_krb5'], pam)
    assert content == expected


# repos/system_upgrade/el7toel8/actors/removeoldpammodulesscanner/libraries/removeoldpammodulesscanner.py

from leapp.models import RemovedPAMModules


class RemoveOldPAMModulesScannerLibrary(object):
    """
    Scan PAM configuration for modules that are not available in RHEL-8.

    PAM module pam_krb5 and pam_pkcs11 are no longer present in RHEL-8
    and must be removed from PAM configuration, otherwise it may lock out
    the system.
    """

    def __init__(self, pam):
        self.pam = pam

    def process(self):
        # PAM modules pam_pkcs11 and pam_krb5 are no longer available in
        # RHEL8. We must remove them because if they are left in PAM
        # configuration it may lock out the system.
        modules = []
        for module in ['pam_krb5', 'pam_pkcs11']:
            if self.pam.has(module):
                modules.append(module)

        return RemovedPAMModules(
            modules=modules
        )


# repos/system_upgrade/el7toel8/actors/removeoldpammodulesscanner/tests/unit_test_removeoldpammodulesscanner.py

import textwrap

from leapp.libraries.actor.removeoldpammodulesscanner import RemoveOldPAMModulesScannerLibrary
from leapp.libraries.common.pam import PAM


def get_config(config):
    return textwrap.dedent(config).strip()


def test_RemoveOldPAMModulesScannerLibrary_process__pkcs11():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_pkcs11.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = RemoveOldPAMModulesScannerLibrary(PAM(pam))
    model = obj.process()
    assert model.modules == ['pam_pkcs11']


def test_RemoveOldPAMModulesScannerLibrary_process__krb5():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_krb5.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = RemoveOldPAMModulesScannerLibrary(PAM(pam))
    model = obj.process()
    assert model.modules == ['pam_krb5']


def test_RemoveOldPAMModulesScannerLibrary_process__all():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_krb5.so
    auth sufficient pam_pkcs11.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = RemoveOldPAMModulesScannerLibrary(PAM(pam))
    model = obj.process()
    assert len(model.modules) == 2
    assert 'pam_krb5' in model.modules
    assert 'pam_pkcs11' in model.modules


def test_RemoveOldPAMModulesScannerLibrary_process__none():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = RemoveOldPAMModulesScannerLibrary(PAM(pam))
    model = obj.process()
    assert not model.modules


# repos/system_upgrade/el7toel8/actors/reportleftoverpackages/reportleftoverpackages/actor.py

from leapp.actors import Actor
from leapp.models import LeftoverPackages, RemovedPackages
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import RPMUpgradePhaseTag, IPUWorkflowTag


class ReportLeftoverPackages(Actor):
    """
    Collect messages about leftover el7 packages and generate report for users.

    Depending on execution of previous actors, generated report contains information that there are still el7 packages
    present on the system, which makes it unsupported or lists packages that have been removed.
    """

    name = 'report_leftover_packages'
    consumes = (LeftoverPackages, RemovedPackages)
    produces = (Report,)
    tags = (RPMUpgradePhaseTag, IPUWorkflowTag)

    def process(self):
        removed_packages = next(self.consume(RemovedPackages), None)
        leftover_packages = next(self.consume(LeftoverPackages), LeftoverPackages())
        to_remove = ['-'.join([pkg.name, pkg.version, pkg.release]) for pkg in leftover_packages.items]

        if removed_packages:
            title = 'Leftover RHEL 7 packages have been removed'

            if removed_packages.items:
                removed = ['-'.join([pkg.name, pkg.version, pkg.release]) for pkg in removed_packages.items]
                create_report([
                    reporting.Title(title),
                    reporting.Summary('Following packages have been removed:\n{}'.format('\n'.join(removed))),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Tags([reporting.Tags.SANITY]),
                ] + [reporting.RelatedResource('package', pkg.name) for pkg in removed_packages.items])
            else:
                summary = ('Following packages have been removed:\n'
                           '{}\n'
                           'Dependent packages may have been removed as well, please check that you are not missing '
                           'any packages.\n'.format('\n'.join(to_remove)))

                create_report([
                    reporting.Title(title),
                    reporting.Summary(summary),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Tags([reporting.Tags.SANITY]),
                ] + [reporting.RelatedResource('package', pkg.name) for pkg in leftover_packages.items])
            return

        if not leftover_packages.items:
            self.log.info('No leftover packages, skipping...')
            return

        summary = 'Following RHEL 7 packages have not been upgraded:\n{}\n'.format('\n'.join(to_remove))
        summary += 'Please remove these packages to keep your system in supported state.\n'
        create_report([
            reporting.Title('Some RHEL 7 packages have not been upgraded'),
            reporting.Summary(summary),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SANITY]),
        ] + [reporting.RelatedResource('package', pkg.name) for pkg in leftover_packages.items])


# repos/system_upgrade/el7toel8/actors/repositoriesblacklist/libraries/repositoriesblacklist.py

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepository, RepositoriesBlacklisted, RepositoriesFacts, RepositoriesMapping

# {OS_MAJOR_VERSION: PESID}
UNSUPPORTED_PESIDS = {
    "7": "rhel7-optional",
    "8": "rhel8-CRB",
    "9": "rhel9-CRB"
}


def _report_using_unsupported_repos(repos):
    report = [
        reporting.Title("Using repository not supported by Red Hat"),
        reporting.Summary(
            "The following repositories have been used for the "
            "upgrade, but they are not supported by the Red Hat.:\n"
            "- {}".format("\n - ".join(repos))
        ),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Tags([reporting.Tags.REPOSITORY]),
    ]
    reporting.create_report(report)


def _report_excluded_repos(repos):
    api.current_logger().info(
        "The optional repository is not enabled. Excluding %r "
        "from the upgrade",
        repos,
    )

    report = [
        reporting.Title("Excluded target system repositories"),
        reporting.Summary(
            "The following repositories are not supported by "
            "Red Hat and are excluded from the list of repositories "
            "used during the upgrade.\n- {}".format("\n- ".join(repos))
        ),
        reporting.Severity(reporting.Severity.INFO),
        reporting.Tags([reporting.Tags.REPOSITORY]),
        reporting.Flags([reporting.Flags.FAILURE]),
        reporting.Remediation(
            hint=(
                "If some of excluded repositories are still required to be used"
                " during the upgrade, execute leapp with the --enablerepo option"
                " with the repoid of the repository required to be enabled"
                " as an argument (the option can be used multiple times)."
            )
        ),
    ]
    reporting.create_report(report)


def _get_manually_enabled_repos():
    """
    Get a set of repositories (repoids) that are manually enabled.

    manually enabled means (
        specified by --enablerepo option of the leapp command or
        inside the /etc/leapp/files/leapp_upgrade_repositories.repo),
    )
    :rtype: set [repoid]
    """
    try:
        return {repo.repoid for repo in api.consume(CustomTargetRepository)}
    except StopIteration:
        return set()


def _get_pesid_repos(repo_mapping, pesid, major_version):
    """
    Returns a list of pesid repos with the specified pesid and major version.

    :param str pesid: The PES ID representing the family of repositories.
    :param str major_version: The major version of the RHEL OS.
    :returns: A set of repoids with the specified pesid and major version.
    :rtype: List[PESIDRepositoryEntry]
    """
    pesid_repos = []
    for pesid_repo in repo_mapping.repositories:
        if pesid_repo.pesid == pesid and pesid_repo.major_version == major_version:
            pesid_repos.append(pesid_repo)
    return pesid_repos


def _get_repoids_to_exclude(repo_mapping):
    """
    Returns a set of repoids that should be blacklisted on the target system.

    :param RepositoriesMapping repo_mapping: Repository mapping data.
    :returns: A set of repoids to blacklist on the target system.
    :rtype: Set[str]
    """
    pesid_repos_to_exclude = _get_pesid_repos(repo_mapping,
                                              UNSUPPORTED_PESIDS[get_target_major_version()],
                                              get_target_major_version())
    return {pesid_repo.repoid for pesid_repo in pesid_repos_to_exclude}


def _are_optional_repos_disabled(repo_mapping, repos_on_system):
    """
    Checks whether all optional repositories are disabled.

    :param RepositoriesMapping repo_mapping: Repository mapping data.
    :param RepositoriesFacts repos_on_system: Installed repositories on the source system.
    :returns: True if there are any optional repositories enabled on the source system.
    """

    # Get a set of all repo_ids that are optional
    optional_pesid_repos = _get_pesid_repos(repo_mapping,
                                            UNSUPPORTED_PESIDS[get_source_major_version()],
                                            get_source_major_version())

    optional_repoids = [optional_pesid_repo.repoid for optional_pesid_repo in optional_pesid_repos]

    # Gather all optional repositories on the source system that are not enabled
    for repofile in repos_on_system.repositories:
        for repository in repofile.data:
            if repository.repoid in optional_repoids and repository.enabled:
                return False
    return True


def process():
    """
    Exclude target repositories provided by Red Hat without support.

    Conditions to exclude:
    - there are not such repositories already enabled on the source system
      (e.g. "Optional" repositories)
    - such repositories are not required for the upgrade explicitly by the user
      (e.g. via the --enablerepo option or via the /etc/leapp/files/leapp_upgrade_repositories.repo file)

    E.g. CRB repository is provided by Red Hat but it is without the support.
    """

    repo_mapping = next(api.consume(RepositoriesMapping), None)
    repos_facts = next(api.consume(RepositoriesFacts), None)

    # Handle required messages not received
    missing_messages = []
    if not repo_mapping:
        missing_messages.append('RepositoriesMapping')
    if not repos_facts:
        missing_messages.append('RepositoriesFacts')
    if missing_messages:
        raise StopActorExecutionError('Actor didn\'t receive required messages: {0}'.format(
            ', '.join(missing_messages)
        ))

    if not _are_optional_repos_disabled(repo_mapping, repos_facts):
        # nothing to do - an optional repository is enabled
        return

    # Optional repos are either not present or they are present, but disabled -> blacklist them on target system
    repos_to_exclude = _get_repoids_to_exclude(repo_mapping)

    # Do not exclude repos manually enabled from the CLI
    manually_enabled_repos = _get_manually_enabled_repos() & repos_to_exclude
    filtered_repos_to_exclude = repos_to_exclude - manually_enabled_repos

    if manually_enabled_repos:
        _report_using_unsupported_repos(manually_enabled_repos)
    if filtered_repos_to_exclude:
        _report_excluded_repos(filtered_repos_to_exclude)
        api.produce(RepositoriesBlacklisted(repoids=list(filtered_repos_to_exclude)))


# repos/system_upgrade/el7toel8/actors/repositoriesblacklist/tests/test_repositoriesblacklist.py

import pytest

from leapp import reporting
from leapp.libraries.actor import repositoriesblacklist
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
    CustomTargetRepository,
    EnvVar,
    PESIDRepositoryEntry,
    RepoMapEntry,
    RepositoriesBlacklisted,
    RepositoriesFacts,
    RepositoriesMapping,
    RepositoryData,
    RepositoryFile
)


@pytest.fixture
def repofacts_opts_disabled():
    repos_data = [
        RepositoryData(
            repoid="rhel-7-server-optional-rpms",
            name="RHEL 7 Server",
            enabled=False,
        )
    ]
    repos_files = [
        RepositoryFile(file="/etc/yum.repos.d/redhat.repo", data=repos_data)
    ]
    return RepositoriesFacts(repositories=repos_files)


@pytest.fixture
def rhel7_optional_pesidrepo():
    return PESIDRepositoryEntry(
        pesid='rhel7-optional',
        major_version='7',
        repoid='rhel-7-server-optional-rpms',
        rhui='',
        arch='x86_64',
        channel='ga',
        repo_type='rpm',
    )


@pytest.fixture
def rhel8_crb_pesidrepo():
    return PESIDRepositoryEntry(
        pesid='rhel8-CRB',
        major_version='8',
        repoid='codeready-builder-for-rhel-8-x86_64-rpms',
        rhui='',
        arch='x86_64',
        channel='ga',
        repo_type='rpm')


@pytest.fixture
def repomap_opts_only(rhel7_optional_pesidrepo, rhel8_crb_pesidrepo):
    return RepositoriesMapping(
        mapping=[RepoMapEntry(source='rhel7-optional', target=['rhel8-CRB'])],
        repositories=[rhel7_optional_pesidrepo, rhel8_crb_pesidrepo]
    )


def test_all_target_optionals_blacklisted_when_no_optional_on_source(monkeypatch, repomap_opts_only):
    """
    Tests whether every target optional repository gets blacklisted
    if no optional repositories are used on the source system.
    """

    repos_data = [
        RepositoryData(
            repoid="rhel-7-server-rpms",
            name="RHEL 7 Server",
            enabled=True,
        )
    ]
    repos_files = [
        RepositoryFile(file="/etc/yum.repos.d/redhat.repo", data=repos_data)
    ]
    repo_facts = RepositoriesFacts(repositories=repos_files)

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repo_facts, repomap_opts_only]))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(reporting, 'create_report', produce_mocked())

    repositoriesblacklist.process()

    assert api.produce.called
    assert 'codeready-builder-for-rhel-8-x86_64-rpms' in api.produce.model_instances[0].repoids


def test_with_no_mapping_for_optional_repos(monkeypatch, repomap_opts_only, repofacts_opts_disabled):
    """
    Tests whether nothing gets produced if no valid target is found for an optional repository in mapping data.
    """

    repomap_opts_only.repositories[1].pesid = 'test_pesid'

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled, repomap_opts_only]))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    repositoriesblacklist.process()

    assert not api.produce.called


def test_blacklist_produced_when_optional_repo_disabled(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
    """
    Tests whether a correct blacklist is generated when there is disabled optional repo on the system.
    """

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled, repomap_opts_only]))
    monkeypatch.setattr(api, "produce", produce_mocked())
    monkeypatch.setattr(reporting, "create_report", produce_mocked())

    repositoriesblacklist.process()

    assert api.produce.model_instances, 'A blacklist should get generated.'

    expected_blacklisted_repoid = 'codeready-builder-for-rhel-8-x86_64-rpms'
    err_msg = 'Blacklist does not contain expected repoid.'
    assert expected_blacklisted_repoid in api.produce.model_instances[0].repoids, err_msg


def test_no_blacklist_produced_when_optional_repo_enabled(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
    """
    Tests whether nothing is produced when an optional repository is enabled.

    Data are set up in such a fashion so that the determined blacklist would not be empty.
    """

    repofacts_opts_disabled.repositories[0].data[0].enabled = True

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled, repomap_opts_only]))
    monkeypatch.setattr(api, "produce", produce_mocked())
    monkeypatch.setattr(reporting, "create_report", produce_mocked())

    repositoriesblacklist.process()

    assert not api.produce.called


def test_repositoriesblacklist_not_empty(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
    """
    Tests whether a message containing correct packages from the determined blacklist is produced.
    """

    blacklisted_repoid = 'test'
    monkeypatch.setattr(repositoriesblacklist, "_get_repoids_to_exclude", lambda dummy_mapping: {blacklisted_repoid})
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[repofacts_opts_disabled, repomap_opts_only]))
    monkeypatch.setattr(api, "produce", produce_mocked())
    monkeypatch.setattr(reporting, "create_report", produce_mocked())

    repositoriesblacklist.process()
    assert api.produce.called == 1
    assert isinstance(api.produce.model_instances[0], RepositoriesBlacklisted)
    assert api.produce.model_instances[0].repoids[0] == blacklisted_repoid
    assert reporting.create_report.called == 1


def test_repositoriesblacklist_empty(monkeypatch, repofacts_opts_disabled, repomap_opts_only):
    """
    Tests whether nothing is produced if there are some disabled optional repos, but an empty blacklist is determined
    from the repo mapping data.
    """

    msgs_to_feed = [repofacts_opts_disabled, repomap_opts_only]

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs_to_feed))
    monkeypatch.setattr(
        repositoriesblacklist,
        "_get_repoids_to_exclude",
        lambda dummy_mapping: set()
    )  # pylint:disable=W0108
    monkeypatch.setattr(api, "produce", produce_mocked())

    repositoriesblacklist.process()
    assert api.produce.called == 0


@pytest.mark.parametrize(
    ("enabled_repo", "exp_report_title", "message_produced"),
    [
        ("codeready-builder-for-rhel-8-x86_64-rpms", "Using repository not supported by Red Hat", False),
        ("some_other_enabled_repo", "Excluded target system repositories", True),
        (None, "Excluded target system repositories", True),
    ],
)
def test_enablerepo_option(monkeypatch,
                           repofacts_opts_disabled,
                           repomap_opts_only,
                           enabled_repo,
                           exp_report_title,
                           message_produced):
    """
    Tests whether the actor respects CustomTargetRepository messages when constructing the blacklist.
    """

    msgs_to_feed = [repomap_opts_only, repofacts_opts_disabled]

    if enabled_repo:
        msgs_to_feed.append(CustomTargetRepository(repoid=enabled_repo))
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs_to_feed))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    repositoriesblacklist.process()
    assert reporting.create_report.report_fields["title"] == exp_report_title
    if message_produced:
        assert isinstance(api.produce.model_instances[0], RepositoriesBlacklisted)
    else:
        assert not api.produce.model_instances


# repos/system_upgrade/el7toel8/actors/sanebackendsmigrate/libraries/sanebackendsmigrate.py

from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM


# Database of changes in configuration files of sane-backends
# between RHELs

CANON_DR = [
    '# P-150M',
    'usb 0x1083 0x162c',
    '# DR-M160',
    'option extra-status 1',
    'option duplex-offset 400',
    'usb 0x1083 0x163e',
    '# DR-M140',
    'option extra-status 1',
    'option duplex-offset 400',
    'usb 0x1083 0x163f',
    '# DR-C125',
    'option duplex-offset 400',
    'usb 0x1083 0x1640',
    '# DR-P215',
    'usb 0x1083 0x1641',
    '# FSU-201',
    'usb 0x1083 0x1648',
    '# DR-C130',
    'usb 0x1083 0x164a',
    '# DR-P208',
    'usb 0x1083 0x164b',
    '# DR-G1130',
    'option buffer-size 8000000',
    'usb 0x1083 0x164f',
    '# DR-G1100',
    'option buffer-size 8000000',
    'usb 0x1083 0x1650',
    '# DR-C120',
    'usb 0x1083 0x1651',
    '# P-201',
    'usb 0x1083 0x1652',
    '# DR-F120',
    'option duplex-offset 1640',
    'usb 0x1083 0x1654',
    '# DR-M1060',
    'usb 0x1083 0x1657',
    '# DR-C225',
    'usb 0x1083 0x1658',
    '# DR-P215II',
    'usb 0x1083 0x1659',
    '# P-215II',
    'usb 0x1083 0x165b',
    '# DR-P208II',
    'usb 0x1083 0x165d',
    '# P-208II',
    'usb 0x1083 0x165f'
]

CARDSCAN = [
    '# Sanford Cardscan 800c',
    'usb 0x0451 0x6250'
]

DLL = ['epsonds']

EPJITSU = [
    '# Fujitsu fi-65F',
    'firmware /usr/share/sane/epjitsu/65f_0A01.nal',
    'usb 0x04c5 0x11bd',
    '# Fujitsu S1100',
    'firmware /usr/share/sane/epjitsu/1100_0B00.nal',
    'usb 0x04c5 0x1200',
    '# Fujitsu S1300i',
    'firmware /usr/share/sane/epjitsu/1300i_0D12.nal',
    'usb 0x04c5 0x128d',
    '# Fujitsu S1100i',
    'firmware /usr/share/sane/epjitsu/1100i_0A00.nal',
    'usb 0x04c5 0x1447'
]

FUJITSU = [
    '#fi-6125',
    'usb 0x04c5 0x11ee',
    '#fi-6225',
    'usb 0x04c5 0x11ef',
    '#ScanSnap SV600',
    'usb 0x04c5 0x128e',
    '#fi-7180',
    'usb 0x04c5 0x132c',
    '#fi-7280',
    'usb 0x04c5 0x132d',
    '#fi-7160',
    'usb 0x04c5 0x132e',
    '#fi-7260',
    'usb 0x04c5 0x132f',
    '#ScanSnap iX500EE',
    'usb 0x04c5 0x13f3',
    '#ScanSnap iX100',
    'usb 0x04c5 0x13f4',
    '#ScanPartner SP25',
    'usb 0x04c5 0x1409',
    '#ScanPartner SP30',
    'usb 0x04c5 0x140a',
    '#ScanPartner SP30F',
    'usb 0x04c5 0x140c',
    '#fi-6140ZLA',
    'usb 0x04c5 0x145f',
    '#fi-6240ZLA',
    'usb 0x04c5 0x1460',
    '#fi-6130ZLA',
    'usb 0x04c5 0x1461',
    '#fi-6230ZLA',
    'usb 0x04c5 0x1462',
    '#fi-6125ZLA',
    'usb 0x04c5 0x1463',
    '#fi-6225ZLA',
    'usb 0x04c5 0x1464',
    '#fi-6135ZLA',
    'usb 0x04c5 0x146b',
    '#fi-6235ZLA',
    'usb 0x04c5 0x146c',
    '#fi-6120ZLA',
    'usb 0x04c5 0x146d',
    '#fi-6220ZLA',
    'usb 0x04c5 0x146e',
    '#N7100',
    'usb 0x04c5 0x146f',
    '#fi-6400',
    'usb 0x04c5 0x14ac',
    '#fi-7480',
    'usb 0x04c5 0x14b8',
    '#fi-6420',
    'usb 0x04c5 0x14bd',
    '#fi-7460',
    'usb 0x04c5 0x14be',
    '#fi-7140',
    'usb 0x04c5 0x14df',
    '#fi-7240',
    'usb 0x04c5 0x14e0',
    '#fi-7135',
    'usb 0x04c5 0x14e1',
    '#fi-7235',
    'usb 0x04c5 0x14e2',
    '#fi-7130',
    'usb 0x04c5 0x14e3',
    '#fi-7230',
    'usb 0x04c5 0x14e4',
    '#fi-7125',
    'usb 0x04c5 0x14e5',
    '#fi-7225',
    'usb 0x04c5 0x14e6',
    '#fi-7120',
    'usb 0x04c5 0x14e7',
    '#fi-7220',
    'usb 0x04c5 0x14e8',
    '#fi-400F',
    'usb 0x04c5 0x151e',
    '#fi-7030',
    'usb 0x04c5 0x151f',
    '#fi-7700',
    'usb 0x04c5 0x1520',
    '#fi-7600',
    'usb 0x04c5 0x1521',
    '#fi-7700S',
    'usb 0x04c5 0x1522'
]

CANON = [
    '# Canon LiDE 80',
    'usb 0x04a9 0x2214',
    '# Canon LiDE 120',
    'usb 0x04a9 0x190e',
    '# Canon LiDE 220',
    'usb 0x04a9 0x190f'
]

XEROX_MFP = [
    '#Samsung X4300 Series',
    'usb 0x04e8 0x3324',
    '#Samsung K4350 Series',
    'usb 0x04e8 0x3325',
    '#Samsung X7600 Series',
    'usb 0x04e8 0x3326',
    '#Samsung K7600 Series',
    'usb 0x04e8 0x3327',
    '#Samsung K703 Series',
    'usb 0x04e8 0x3331',
    '#Samsung X703 Series',
    'usb 0x04e8 0x3332',
    '#Samsung M458x Series',
    'usb 0x04e8 0x346f',
    '#Samsung M4370 5370 Series',
    'usb 0x04e8 0x3471',
    '#Samsung X401 Series',
    'usb 0x04e8 0x3477',
    '#Samsung K401 Series',
    'usb 0x04e8 0x3478',
    '#Samsung K3250 Series',
    'usb 0x04e8 0x3481',
    '#Samsung X3220 Series',
    'usb 0x04e8 0x3482'
]

NEW_QUIRKS = {
    '/etc/sane.d/canon_dr.conf': CANON_DR,
    '/etc/sane.d/cardscan.conf': CARDSCAN,
    '/etc/sane.d/dll.conf': DLL,
    '/etc/sane.d/epjitsu.conf': EPJITSU,
    '/etc/sane.d/fujitsu.conf': FUJITSU,
    '/etc/sane.d/canon.conf': CANON,
    '/etc/sane.d/xerox_mfp.conf': XEROX_MFP
}
"""
Dictionary of configuration files which changes in 1.0.27
"""


def _macro_exists(path, macro):
    """
    Check if macro is in the file.

    :param str path: string representing the full path of the config file
    :param str macro: new directive to be added
    :return boolean res: macro does/does not exist in the file
    """
    with open(path, 'r') as f:
        lines = f.readlines()

    for line in lines:
        if line.lstrip().startswith(macro):
            return True
    return False


def _append_string(path, content):
    """
    Append string at the end of file.

    :param str path: string representing the full path of file
    :param str content: preformatted string to be added
    """
    with open(path, 'a') as f:
        f.write(content)


def update_config(path,
                  quirks,
                  check_function=_macro_exists,
                  append_function=_append_string):
    """
    Insert expected content into the file on the path if it is not
    in the file already.

    :param str path: string representing the full path of the config file
    :param func check_function: function to be used to check if string is in the file
    :param func append_function: function to be used to append string
    """

    macros = []
    for macro in quirks:
        if not check_function(path, macro):
            macros.append(macro)

    if not macros:
        return

    fmt_input = "\n{comment_line}\n{content}\n".format(comment_line='# content added by Leapp',
                                                       content='\n'.join(macros))

    try:
        append_function(path, fmt_input)
    except IOError:
        raise IOError('Error during writing to file: {}.'.format(path))


def _check_package(pkg_name):
    """
    Checks if the package is installed and signed by Red Hat

    :param str pkg_name: name of package
    """

    return has_package(InstalledRedHatSignedRPM, pkg_name)


def update_sane(debug_log=api.current_logger().debug,
                error_log=api.current_logger().error,
                is_installed=_check_package,
                append_function=_append_string,
                check_function=_macro_exists):
    """
    Iterate over dictionary and updates each configuration file.

    :param func debug_log: function for debug logging
    :param func error_log: function for error logging
    :param func is_installed: checks if the package is installed
    :param func append_function: appends a string into file
    :param func check_function: checks if a string exists in file
    """

    error_list = []

    if not is_installed('sane-backends'):
        return

    for path, lines in NEW_QUIRKS.items():

        debug_log('Updating SANE configuration file {}.'.format(path))

        try:
            update_config(path, lines, check_function, append_function)
        except (OSError, IOError) as error:
            error_list.append((path, error))

    if error_list:
        error_log('The files below have not been modified '
                  '(error message included):' +
                  ''.join(['\n    - {}: {}'.format(err[0], err[1])
                          for err in error_list]))
        return


# repos/system_upgrade/el7toel8/actors/sanebackendsmigrate/tests/test_update_config_sanebackendsmigrate.py

import pytest

from leapp.libraries.actor.sanebackendsmigrate import (
    CANON,
    CANON_DR,
    CARDSCAN,
    DLL,
    EPJITSU,
    FUJITSU,
    XEROX_MFP,
    update_config
)


def _pattern_exists(content, macro):
    for line in content.split('\n'):
        if line.lstrip().startswith(macro):
            return True
    return False


def _create_original_file(file_content):
    content = ''
    for line in file_content:
        fmt_line = '{}\n'.format(line)
        content += fmt_line
    return content


def _create_expected_file(original_content, new_content):
    macros = []
    for line in new_content:
        if not _pattern_exists(original_content, line):
            macros.append(line)

    fmt_input = ''
    if macros:
        fmt_input = "\n{comment_line}\n{content}\n".format(comment_line='# content added by Leapp',
                                                           content='\n'.join(macros))

    return '\n'.join((original_content, fmt_input))


testdata = [
    (
        _create_original_file(['']),
        _create_expected_file('', CANON),
        CANON
    ),
    (
        _create_original_file(['']),
        _create_expected_file('', CANON_DR),
        CANON_DR
    ),
    (
        _create_original_file(['']),
        _create_expected_file('', CARDSCAN),
        CARDSCAN
    ),
    (
        _create_original_file(['']),
        _create_expected_file('', DLL),
        DLL
    ),
    (
        _create_original_file(['']),
        _create_expected_file('', EPJITSU),
        EPJITSU
    ),
    (
        _create_original_file(['']),
        _create_expected_file('', FUJITSU),
        FUJITSU
    ),
    (
        _create_original_file(['']),
        _create_expected_file('', XEROX_MFP),
        XEROX_MFP
    ),
    (
        _create_original_file(['fdfdfdr']),
        _create_expected_file('fdfdfdr', CANON),
        CANON
    ),
    (
        _create_original_file(['fdfdfdr']),
        _create_expected_file('fdfdfdr', CANON_DR),
        CANON_DR
    ),
    (
        _create_original_file(['fdfdfdr']),
        _create_expected_file('fdfdfdr', CARDSCAN),
        CARDSCAN
    ),
    (
        _create_original_file(['fdfdfdr']),
        _create_expected_file('fdfdfdr', DLL),
        DLL
    ),
    (
        _create_original_file(['fdfdfdr']),
        _create_expected_file('fdfdfdr', EPJITSU),
        EPJITSU
    ),
    (
        _create_original_file(['fdfdfdr']),
        _create_expected_file('fdfdfdr', FUJITSU),
        FUJITSU
    ),
    (
        _create_original_file(['fdfdfdr']),
        _create_expected_file('fdfdfdr', XEROX_MFP),
        XEROX_MFP
    ),
    (
        _create_original_file(['usb 0x04a9 0x2214']),
        _create_expected_file('usb 0x04a9 0x2214', CANON),
        CANON
    ),
    (
        _create_original_file(['usb 0x1083 0x162c']),
        _create_expected_file('usb 0x1083 0x162c', CANON_DR),
        CANON_DR
    ),
    (
        _create_original_file(['usb 0x0451 0x6250']),
        _create_expected_file('usb 0x0451 0x6250', CARDSCAN),
        CARDSCAN
    ),
    (
        _create_original_file(['#usb 0x0451 0x6250']),
        _create_expected_file('#usb 0x0451 0x6250', CARDSCAN),
        CARDSCAN
    ),
    (
        _create_original_file(['epsonds']),
        _create_expected_file('epsonds', DLL),
        DLL
    ),
    (
        _create_original_file(['usb 0x04c5 0x11bd']),
        _create_expected_file('usb 0x04c5 0x11bd', EPJITSU),
        EPJITSU
    ),
    (
        _create_original_file(['usb 0x04c5 0x132c']),
        _create_expected_file('usb 0x04c5 0x132c', FUJITSU),
        FUJITSU
    ),
    (
        _create_original_file(['usb 0x04e8 0x3471']),
        _create_expected_file('usb 0x04e8 0x3471', XEROX_MFP),
        XEROX_MFP
    ),
    (
        _create_original_file(CANON),
        _create_original_file(CANON),
        CANON
    ),
    (
        _create_original_file(CANON_DR),
        _create_original_file(CANON_DR),
        CANON_DR
    ),
    (
        _create_original_file(CARDSCAN),
        _create_original_file(CARDSCAN),
        CARDSCAN
    ),
    (
        _create_original_file(DLL),
        _create_original_file(DLL),
        DLL
    ),
    (
        _create_original_file(EPJITSU),
        _create_original_file(EPJITSU),
        EPJITSU
    ),
    (
        _create_original_file(FUJITSU),
        _create_original_file(FUJITSU),
        FUJITSU
    ),
    (
        _create_original_file(XEROX_MFP),
        _create_original_file(XEROX_MFP),
        XEROX_MFP
    )
]
"""
3-tuple of original file, file after migration and list of lines which
will be tried to add
"""


class MockFile(object):
    def __init__(self, path, content=None):
        self.path = path
        self.content = content
        self.error = False

    def append(self, path, content):
        if path != self.path:
            self.error = True
        if not self.error:
            self.content += content
            return self.content
        raise IOError('Error during writing to file: {}.'.format(path))

    def exists(self, path, macro):
        for line in self.content.split('\n'):
            if line.lstrip().startswith(macro) and self.path == path:
                return True
        return False


def test_update_config_file_errors():
    path = 'foo'
    new_content = ['fdfgdfg', 'gnbfgnf']

    f = MockFile(path, content='')

    with pytest.raises(IOError):
        update_config('bar', new_content, f.exists, f.append)

    assert f.content == ''


@pytest.mark.parametrize('orig_content,expected_result,content_to_add', testdata)
def test_update_config_append_into_file(orig_content,
                                        expected_result,
                                        content_to_add):
    f = MockFile('foo', orig_content)

    update_config('foo', content_to_add, f.exists, f.append)

    assert f.content == expected_result


# repos/system_upgrade/el7toel8/actors/sanebackendsmigrate/tests/test_update_sane_sanebackendsmigrate.py

import pytest

from leapp.libraries.actor.sanebackendsmigrate import update_sane, NEW_QUIRKS


testdata = [
    {'sane-backends': '/etc/sane.d/canon_dr.conf'},
    {'sane-backends': ''},
    {'ble': ''}
]


class MockLogger(object):
    def __init__(self):
        self.debugmsg = ''
        self.errmsg = ''

    def debug(self, message):
        self.debugmsg += message

    def error(self, message):
        self.errmsg += message


class MockPackage(object):
    def __init__(self, name, config):
        self.name = name
        self.config = config
        self.config_content = ''


class MockPackageSet(object):
    def __init__(self):
        self.installed_packages = None

    def add_packages(self, pkgs):
        if self.installed_packages is None:
            self.installed_packages = []

        for rpm, config in pkgs.items():
            self.installed_packages.append(MockPackage(rpm, config))

    def is_installed(self, pkg):
        for rpm in self.installed_packages:
            if pkg == rpm.name:
                return True
        return False

    def append_content(self, path, content):
        found = False

        for rpm in self.installed_packages:
            if path == rpm.config:
                found = True
                rpm.config_content += content
        if not found:
            raise IOError('Error during writing to file: {}.'.format(path))

    def check_content(self, path, content):
        found = False

        for rpm in self.installed_packages:
            if path == rpm.config and content in rpm.config_content:
                found = True

        return found


class ExpectedOutput(object):
    def __init__(self):
        self.debugmsg = ''
        self.errmsg = ''

    def create(self, rpms):
        error_list = []
        found = False

        for pkg, config in rpms.items():
            if pkg == 'sane-backends':
                found = True
                break

        if found:
            for sane_config in NEW_QUIRKS.keys():
                self.debugmsg += ('Updating SANE configuration file {}.'
                                  .format(sane_config))
                if config == '' or config != sane_config:
                    error_list.append((sane_config,
                                       'Error during writing to file: {}.'
                                       .format(sane_config)))

        if error_list:
            self.errmsg = ('The files below have not been modified '
                           '(error message included):' +
                           ''.join(['\n    - {}: {}'.format(err[0], err[1])
                                   for err in error_list]))


@pytest.mark.parametrize("rpms", testdata)
def test_actor_check_report(rpms):
    logger = MockLogger()
    installed_packages = MockPackageSet()

    installed_packages.add_packages(rpms)

    expected = ExpectedOutput()
    expected.create(rpms)

    update_sane(logger.debug,
                logger.error,
                installed_packages.is_installed,
                installed_packages.append_content,
                installed_packages.check_content)

    assert expected.debugmsg == logger.debugmsg
    assert expected.errmsg == logger.errmsg


# repos/system_upgrade/el7toel8/actors/sctpchecks/tests/component_test_sctpchecks.py

from leapp.actors import Actor
from leapp.models import SCTPConfig, RpmTransactionTasks
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


def test_sctp_wanted(current_actor_context):
    current_actor_context.feed(SCTPConfig(wanted=True))
    current_actor_context.run()
    assert current_actor_context.consume(RpmTransactionTasks)
    assert current_actor_context.consume(RpmTransactionTasks)[0].to_install == ['kernel-modules-extra']


def test_sctp_empty_config(current_actor_context):
    current_actor_context.feed(SCTPConfig())
    current_actor_context.run()
    assert not current_actor_context.consume(RpmTransactionTasks)


def test_sctp_no_config(current_actor_context):
    current_actor_context.run()
    assert not current_actor_context.consume(RpmTransactionTasks)


def test_sctp_unwanted(current_actor_context):
    current_actor_context.feed(SCTPConfig(wanted=False))
    current_actor_context.run()
    assert not current_actor_context.consume(RpmTransactionTasks)


# repos/system_upgrade/el7toel8/actors/sctpconfigread/libraries/sctpdlm.py

#
# Functions for probing SCTP usage by DLM
#
import re

from leapp.libraries.stdlib import api
from leapp.libraries.common import utils


def check_dlm_cfgfile(_open=open):
    """Parse DLM config file.
    :param _open: object behind opening a file. Might be replaced
        by mocked one for the purpose of testing
    """
    fname = '/etc/dlm/dlm.conf'

    try:
        with _open(fname, 'r') as fp:
            cfgs = '[dlm]\n' + fp.read()
    except (OSError, IOError):
        return False

    cfg = utils.parse_config(cfgs)

    if not cfg.has_option('dlm', 'protocol'):
        return False

    proto = cfg.get('dlm', 'protocol').lower()
    return proto in ['sctp', 'detect', '1', '2']


def check_dlm_sysconfig(_open=open):
    """Parse /etc/sysconfig/dlm
    :param _open: object behind opening a file. Might be replaced
        by mocked one for the purpose of testing
    """

    regex = re.compile('^[^#]*DLM_CONTROLD_OPTS.*=.*(?:--protocol|-r)[ =]*([^"\' ]+).*', re.IGNORECASE)

    try:
        with _open('/etc/sysconfig/dlm', 'r') as fp:
            lines = fp.readlines()
    except (OSError, IOError):
        return False

    for line in lines:
        if regex.match(line):
            proto = regex.sub('\\1', line).lower().strip()
            if proto in ['sctp', 'detect']:
                return True

    return False


def is_dlm_using_sctp():
    if check_dlm_cfgfile():
        api.current_logger().info('DLM is configured to use SCTP on dlm.conf.')
        return True

    if check_dlm_sysconfig():
        api.current_logger().info('DLM is configured to use SCTP on sysconfig.')
        return True

    return False


# repos/system_upgrade/el7toel8/actors/sctpconfigread/libraries/sctplib.py

#
# Helper functions
#

from os.path import isfile

from leapp.libraries.actor import sctpdlm
from leapp.libraries.stdlib import CalledProcessError, api, run
from leapp.models import ActiveKernelModulesFacts


def anyfile(files):
    """
    Determines if any of the given paths exist and are a file.

    :type files: tuple of str
    :return: True if any of the given paths exists and it is a file.
    :rtype: bool
    """
    for f in files:
        try:
            if isfile(f):
                return True
        except OSError:
            continue
    return False


def is_module_loaded(module):
    """
    Determines if the given kernel module has been reported in the ActiveKernelModuleFacts as loaded.

    :return: True if the module has been found in the ActiveKernelModuleFacts.
    :rtype: bool
    """
    for fact in api.consume(ActiveKernelModulesFacts):
        for active_module in fact.kernel_modules:
            if active_module.filename == module:
                return True
    return False


def is_sctp_used():
    """
    Logic function that decides whether SCTP is being used on this machine.

    :return: True if SCTP usage was detected.
    :rtype: bool
    """

    # If anything is using SCTP, be it for listening on new connections or
    # connecting somewhere else, the module will be loaded. Thus, no need to
    # also probe on sockets.
    if is_module_loaded('sctp'):
        return True

    # Basic files from lksctp-tools. This check is enough and checking RPM
    # database is an overkill here and this allows for checking for
    # manually installed ones, which is possible.
    lksctp_files = ['/usr/lib64/libsctp.so.1',
                    '/usr/lib/libsctp.so.1',
                    '/usr/bin/sctp_test']
    if anyfile(lksctp_files):
        api.current_logger().debug('At least one of lksctp files is present.')
        return True

    if sctpdlm.is_dlm_using_sctp():
        return True

    return False


def was_sctp_used():
    """
    Determines whether SCTP has been used in the path month, by checking the journalctl.

    :return: True if SCTP usage has been found.
    :rtype: bool
    """
    try:
        run(['check_syslog_for_sctp.sh'])
    except CalledProcessError:
        api.current_logger().debug('Nothing regarding SCTP was found on journal.')
        return False
    api.current_logger().debug('Found logs regarding SCTP on journal.')
    return True


def is_sctp_wanted():
    """
    Decision making funtion that decides based on the current or past usage of SCTP, the SCTP module is wanted
    on the new system.

    :return: True if SCTP seems to be in use or has been recently used.
    :rtype: bool
    """
    if is_sctp_used():
        api.current_logger().info('SCTP is being used.')
        return True

    if was_sctp_used():
        api.current_logger().info('SCTP was used.')
        return True

    api.current_logger().info('SCTP is not being used and neither wanted.')
    return False


# repos/system_upgrade/el7toel8/actors/sctpconfigread/tests/test_unit_sctpconfigread_sctpdlm.py

import logging

import pytest
import six

from leapp.libraries.actor import sctpdlm

if six.PY2:
    from mock import mock_open
else:
    from unittest.mock import mock_open


# TODO Confirm with the team the way to mock builtin open
#   and apply this throughout the repo


@pytest.mark.parametrize(
    ('config', 'open_raises', 'exp_return',),
    [
        ('', IOError, False),
        ('', OSError, False),
        ('log_debug=1\npost_join_delay=10', None, False),
        ('log_debug=1\npost_join_delay=10\nprotocol=sctp', None, True),
        ('log_debug=1\npost_join_delay=10\nprotocol=detect', None, True),
        ('log_debug=1\npost_join_delay=10\nprotocol=1', None, True),
        ('log_debug=1\npost_join_delay=10\nprotocol=2', None, True),
        ('log_debug=1\npost_join_delay=10\nprotocol=tcp', None, False),
        ('log_debug=1\npost_join_delay=10', None, False),
    ],
)
def test_check_dlm_cfgfile(config, open_raises, exp_return):
    if open_raises:
        mock_open.side_effect = open_raises
    assert (
        sctpdlm.check_dlm_cfgfile(_open=mock_open(read_data=config))
        == exp_return
    )


@pytest.mark.parametrize(
    ('config', 'open_raises', 'exp_return'),
    [
        ('', IOError, False),
        ('', OSError, False),
        ('DLM_CONTROLD_OPTS="- f 0 -q 0 --protocol=sctp"', None, True),
        ('DLM_CONTROLD_OPTS="- f 0 -q 0 -r detect"', None, True),
        ('DLM_CONTROLD_OPTS="- f 0 -q 0 --protocol tcp"', None, False),
    ],
)
def test_check_dlm_sysconfig(config, open_raises, exp_return):
    if open_raises:
        mock_open.side_effect = open_raises
    assert (
        sctpdlm.check_dlm_sysconfig(_open=mock_open(read_data=config))
        == exp_return
    )


@pytest.mark.parametrize(
    (
        'check_dlm_cfg_file_returns',
        'check_dlm_sysconfig_returns',
        'exp_return',
        'text_in_log',
    ),
    [
        (True, False, True, 'DLM is configured to use SCTP on dlm.conf.'),
        (False, True, True, 'DLM is configured to use SCTP on sysconfig.'),
        (False, False, False, ''),
    ],
)
def test_is_dlm_using_sctp(
    check_dlm_cfg_file_returns,
    check_dlm_sysconfig_returns,
    exp_return,
    text_in_log,
    monkeypatch,
    caplog,
):
    monkeypatch.setattr(
        sctpdlm, 'check_dlm_cfgfile', lambda: check_dlm_cfg_file_returns
    )
    monkeypatch.setattr(
        sctpdlm, 'check_dlm_sysconfig', lambda: check_dlm_sysconfig_returns
    )
    with caplog.at_level(logging.DEBUG):
        assert sctpdlm.is_dlm_using_sctp() == exp_return
    if text_in_log:
        assert text_in_log in caplog.text


# repos/system_upgrade/el7toel8/actors/sctpconfigread/tests/test_unit_sctpconfigread_sctplib.py

import logging
from functools import partial

import pytest

from leapp.libraries.actor import sctplib, sctpdlm
from leapp.libraries.common.testutils import CurrentActorMocked
from leapp.models import ActiveKernelModulesFacts, ActiveKernelModule

FILENAME_SCTP = 'sctp'
FILENAME_NO_SCTP = 'no_sctp'
SRC_VER = '7.6'

logger = logging.getLogger(__name__)


def test_anyfile(tmpdir):
    file1 = tmpdir.join('file1')
    file2 = tmpdir.join('file2')
    file1.write('I am not empty')
    file2.write('And me either')

    assert sctplib.anyfile((str(file1),))
    assert sctplib.anyfile((str(file1), str(tmpdir)))
    assert not sctplib.anyfile((str(tmpdir),))
    assert not sctplib.anyfile(('Iam not exist',))


def test_is_module_loaded(monkeypatch):
    monkeypatch.setattr(
        sctplib.api,
        'current_actor',
        CurrentActorMocked(
            src_ver=SRC_VER,
            msgs=[
                ActiveKernelModulesFacts(
                    kernel_modules=[
                        ActiveKernelModule(
                            filename=FILENAME_SCTP, parameters=()
                        ),
                    ]
                ),
            ],
        ),
    )
    assert sctplib.is_module_loaded(FILENAME_SCTP)
    assert not sctplib.is_module_loaded('not exists filename')


@pytest.mark.parametrize(
    (
        'actor',
        'exp_return',
        'anyfile_returns',
        'check_dlm_cfgfile_returns',
        'check_dlm_sysconfig_returns',
        'text_in_log',
    ),
    [
        # test if module name is sctp
        (
            CurrentActorMocked(
                src_ver=SRC_VER,
                msgs=[
                    ActiveKernelModulesFacts(
                        kernel_modules=[
                            ActiveKernelModule(
                                filename=FILENAME_SCTP, parameters=()
                            )
                        ]
                    )
                ],
            ),
            True,
            False,
            False,
            False,
            '',
        ),
        # test if module name is different, but one of lksctp is present
        (
            CurrentActorMocked(
                src_ver=SRC_VER,
                msgs=[
                    ActiveKernelModulesFacts(
                        kernel_modules=[
                            ActiveKernelModule(
                                filename=FILENAME_NO_SCTP, parameters=()
                            )
                        ]
                    )
                ],
            ),
            True,
            True,
            False,
            False,
            'lksctp files',
        ),
        # test if check_dlm_cfgfile is True
        (
            CurrentActorMocked(
                src_ver=SRC_VER,
                msgs=[
                    ActiveKernelModulesFacts(
                        kernel_modules=[
                            ActiveKernelModule(
                                filename=FILENAME_NO_SCTP, parameters=()
                            )
                        ]
                    )
                ],
            ),
            True,
            False,
            True,
            False,
            'dlm.conf',
        ),
        # test if check_dlm_sysconfig is True
        (
            CurrentActorMocked(
                src_ver=SRC_VER,
                msgs=[
                    ActiveKernelModulesFacts(
                        kernel_modules=[
                            ActiveKernelModule(
                                filename=FILENAME_NO_SCTP, parameters=()
                            )
                        ]
                    )
                ],
            ),
            True,
            False,
            False,
            True,
            'sysconfig',
        ),
    ],
)
def test_is_sctp_used(
    actor,
    exp_return,
    anyfile_returns,
    check_dlm_cfgfile_returns,
    check_dlm_sysconfig_returns,
    text_in_log,
    monkeypatch,
    caplog,
):
    monkeypatch.setattr(sctplib.api, 'current_actor', actor)
    monkeypatch.setattr(sctplib, 'anyfile', lambda arg: anyfile_returns)
    monkeypatch.setattr(
        sctpdlm, 'check_dlm_cfgfile', lambda: check_dlm_cfgfile_returns
    )
    monkeypatch.setattr(
        sctpdlm, 'check_dlm_sysconfig', lambda: check_dlm_sysconfig_returns
    )
    with caplog.at_level(logging.DEBUG):
        assert sctplib.is_sctp_used() == exp_return
    if text_in_log:
        assert text_in_log in caplog.text


class RunMocked(object):
    """Simple mock class for leapp.libraries.stdlib.run."""

    def __init__(self, exc_type=None):
        """if exc_type provided, then it will be raised on
        instance call.

        :type exc_type: None or BaseException
        """
        self.exc_type = exc_type

    def __call__(self, *args, **kwargs):
        if self.exc_type:
            logger.info('Mocked `run` raising %r', self.exc_type)
            raise self.exc_type()
        logger.info('Mocked `run` passed without exp.')


@pytest.mark.parametrize(
    ('run_fails', 'exp_return', 'text_in_log'),
    [
        (True, False, 'Nothing regarding SCTP was found on journal.'),
        (False, True, 'Found logs regarding SCTP on journal.'),
    ],
)
def test_was_sctp_used(
    monkeypatch, caplog, run_fails, exp_return, text_in_log
):
    monkeypatch.setattr(
        sctplib,
        'run',
        RunMocked(
            exc_type=partial(
                sctplib.CalledProcessError, 'message', 'command', 'result'
            )
            if run_fails
            else None
        ),
    )
    with caplog.at_level(logging.DEBUG):
        assert sctplib.was_sctp_used() == exp_return
    if text_in_log:
        assert text_in_log in caplog.text


@pytest.mark.parametrize(
    (
        'is_sctp_used_returns',
        'was_sctp_used_returns',
        'exp_return',
        'text_in_log',
    ),
    [
        (True, False, True, 'SCTP is being used.'),
        (False, True, True, 'SCTP was used.'),
        (False, False, False, 'SCTP is not being used and neither wanted.'),
    ],
)
def test_is_sctp_wanted(
    is_sctp_used_returns,
    was_sctp_used_returns,
    exp_return,
    text_in_log,
    monkeypatch,
    caplog,
):
    monkeypatch.setattr(sctplib, 'is_sctp_used', lambda: is_sctp_used_returns)
    monkeypatch.setattr(
        sctplib, 'was_sctp_used', lambda: was_sctp_used_returns
    )
    with caplog.at_level(logging.DEBUG):
        assert sctplib.is_sctp_wanted() == exp_return
    if text_in_log:
        assert text_in_log in caplog.text


# repos/system_upgrade/el7toel8/actors/sctpconfigupdate/libraries/sctpupdate.py

import os

from leapp.libraries.stdlib import api, run
from leapp.models import SCTPConfig


def enable_sctp(_modprobe_d_path="/etc/modprobe.d"):
    """
    Enables the SCTP module by removing it from being black listed.
    :type _modprobe_d_path: str
    :param _modprobe_d_path: overwrite only in case of testing, by passing
        some tmp_dir instead
    """

    api.current_logger().info('Enabling SCTP.')
    run(['/usr/bin/sed', '-i', r's/^\s*blacklist.*sctp/#&/',
         os.path.join(_modprobe_d_path, 'sctp_diag-blacklist.conf'),
         os.path.join(_modprobe_d_path, 'sctp-blacklist.conf')])
    api.current_logger().info('Enabled SCTP.')


def perform_update():
    for sctpconfig in api.consume(SCTPConfig):
        api.current_logger().info('Consuming sctp={}'.format(sctpconfig.wanted))
        if sctpconfig.wanted:
            enable_sctp()
            break


# repos/system_upgrade/el7toel8/actors/sctpconfigupdate/tests/test_unit_sctpconfigupdate.py

import logging
import subprocess

import pytest

from leapp.libraries.actor import sctpupdate
from leapp.libraries.stdlib import CalledProcessError

logger = logging.getLogger(__name__)


@pytest.mark.parametrize(
    (
        'conf_content',
        'exp_new_conf_content',
        'log_should_contain',
        'log_shouldnt_contain',
        'conf_files_exists',
        'should_raise_exc',
        'logger_level',
    ),
    [
        # testing normal behaviour
        (
            'blacklist sctp',
            '#blacklist sctp',
            'Enabled SCTP',
            None,
            True,
            None,
            logging.INFO,
        ),
        # testing if regex works also in case sctp just a part of a string
        (
            'blacklist some-sctp',
            '#blacklist some-sctp',
            'Enabled SCTP',
            None,
            True,
            None,
            logging.INFO,
        ),
        # testing if script skips non sctp lines
        (
            'blacklist tcp',
            'blacklist tcp',
            'Enabled SCTP',
            None,
            True,
            None,
            logging.INFO,
        ),
        # testing if the logger message is empty on warning level
        (
            'blacklist tcp',
            'blacklist tcp',
            '',
            None,
            True,
            None,
            logging.WARNING,
        ),
        # testing if CalledProcessError raised when sed exits with non 0 and
        # logger not emits Enabled SCTP (what we want)
        (
            'blacklist tcp',
            'blacklist tcp',
            None,
            'Enabled SCTP',
            False,
            CalledProcessError,
            logging.INFO,
        ),
    ],
)
def test_enable_sctp(
    conf_content,
    exp_new_conf_content,
    log_should_contain,
    log_shouldnt_contain,
    conf_files_exists,
    should_raise_exc,
    logger_level,
    monkeypatch,
    tmpdir,
    caplog,
    capsys,
):
    def mock_run(args):
        logger.info('Calling run with %r', args)
        res = subprocess.call(args)
        if res != 0:
            raise CalledProcessError(
                message='Sed fails with error code {!r}'.format(res),
                command=args,
                result=res,
            )

    monkeypatch.setattr(sctpupdate, 'run', mock_run)

    sctp_diag_blacklist_conf = tmpdir.join('sctp_diag-blacklist.conf')
    sctp_blacklist_conf = tmpdir.join('sctp-blacklist.conf')
    if conf_files_exists:
        sctp_diag_blacklist_conf.write(conf_content)
        sctp_blacklist_conf.write(conf_content)

    with caplog.at_level(logger_level):
        if not should_raise_exc:
            sctpupdate.enable_sctp(_modprobe_d_path=str(tmpdir))
            with open(str(sctp_blacklist_conf)) as conf:
                assert conf.readlines() == [exp_new_conf_content]
        else:
            with pytest.raises(should_raise_exc):
                sctpupdate.enable_sctp(_modprobe_d_path=str(tmpdir))

    if log_should_contain is not None:
        assert log_should_contain in caplog.text
    if log_shouldnt_contain is not None:
        assert log_shouldnt_contain not in caplog.text


# repos/system_upgrade/el7toel8/actors/sourcebootloaderscanner/libraries/sourcebootloaderscanner.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import BootEntry, SourceBootLoaderConfiguration

CMD_GRUBBY_INFO_ALL = ['grubby', '--info', 'ALL']


def parse_grubby_output_line(line):
    """
    Parses a single output line of `grubby --info ALL` that has the property=value format and returns a tuple
    (property, value).

    Quotes are removed from the value.
    :param str line: A line of the grubby output.
    :returns: Tuple containing the key (boot entry property) and its value.
    :rtype: tuple
    """
    line_fragments = line.split('=', 1)
    if len(line_fragments) != 2:
        # The line does not have the property=value format, something is wrong
        raise StopActorExecutionError(
            message='Failed to parse `grubby` output.',
            details={
                'details': 'The following line does not appear to have expected format: {0}'.format(line)
            }
        )

    prop, value = line_fragments
    value = value.strip('\'"')
    return (prop, value)


def scan_boot_entries():
    """
    Scans the available boot entries.

    :rtype: list
    :returns: A list of available boot entries found in the boot loader configuration.
    """
    try:
        grubby_output = run(CMD_GRUBBY_INFO_ALL, split=True)
    except CalledProcessError as err:
        # We have failed to call `grubby` - something is probably wrong here.
        raise StopActorExecutionError(
            message='Failed to call `grubby` to list available boot entries.',
            details={
                'details': str(err),
                'stderr': err.stderr
            }
        )

    boot_entries = []
    boot_entry_data = {}
    for output_line in grubby_output['stdout']:
        if output_line == 'non linux entry':
            # Grubby does not display info about non-linux entries
            # Such an entry is not problematic from our PoV, therefore, skip it
            boot_entry_data = {}
            continue

        prop, value = parse_grubby_output_line(output_line)
        if prop == 'index':
            # Start of a new boot entry section
            if boot_entry_data:
                # There has been a valid linux entry
                boot_entries.append(
                    BootEntry(title=boot_entry_data.get('title', ''),  # In theory, the title property can be missing
                              kernel_image=boot_entry_data['kernel']))
            boot_entry_data = {}
        boot_entry_data[prop] = value

    # There was no 'index=' line after the last boot entry section, thus, its data has not been converted to a model.
    if boot_entry_data:
        boot_entries.append(BootEntry(title=boot_entry_data.get('title', ''),
                                      kernel_image=boot_entry_data['kernel']))
    return boot_entries


def scan_source_boot_loader_configuration():
    """
    Scans the boot loader configuration.

    Produces :class:`SourceBootLoaderConfiguration for other actors to act upon.
    """

    boot_loader_configuration = SourceBootLoaderConfiguration(
        entries=scan_boot_entries()
    )

    api.produce(boot_loader_configuration)


# repos/system_upgrade/el7toel8/actors/sourcebootloaderscanner/tests/test_bootentryscanner.py

import pytest

from leapp.libraries import stdlib
from leapp.libraries.actor import sourcebootloaderscanner
from leapp.libraries.common.testutils import produce_mocked

GRUBBY_INFO_ALL_STDOUT = '''index=0
kernel="/boot/vmlinuz-4.18.0-305.7.1.el8_4.x86_64"
args="ro uned_params"
root="/someroot"
initrd="/boot/initramfs-4.18.0-305.7.1.el8_4.x86_64.img"
title="Linux"
id="some_id"
index=1
kernel="/boot/vmlinuz-4.18.0-305.3.1.el8_4.x86_64"
args="ro"
root="/someroot"
initrd="/boot/initramfs-4.18.0-305.3.1.el8_4.x86_64.img"
title="Linux old-kernel"
id="some_id2"
index=2
non linux entry'''


def test_scan_boot_entries(monkeypatch):
    """Tests whether the library correctly identifies boot entries in the grubby output."""
    def run_mocked(cmd, **kwargs):
        if cmd == ['grubby', '--info', 'ALL']:
            return {
                'stdout': GRUBBY_INFO_ALL_STDOUT.split('\n')
            }
        raise ValueError('Tried to run unexpected command.')

    actor_produces = produce_mocked()

    # The library imports `run` all the way (from ... import run), therefore,
    # we must monkeypatch the reference directly in the actor's library namespace
    monkeypatch.setattr(sourcebootloaderscanner, 'run', run_mocked)
    monkeypatch.setattr(stdlib.api, 'produce', actor_produces)

    sourcebootloaderscanner.scan_source_boot_loader_configuration()

    fail_description = 'Only one SourceBootLoaderConfiguration message should be produced.'
    assert len(actor_produces.model_instances) == 1, fail_description

    bootloader_config = actor_produces.model_instances[0]

    fail_description = 'Found different number of boot entries than present in provided mocks.'
    assert len(bootloader_config.entries) == 2, fail_description

    expected_entries = [
        {'title': 'Linux', 'kernel_image': '/boot/vmlinuz-4.18.0-305.7.1.el8_4.x86_64'},
        {'title': 'Linux old-kernel', 'kernel_image': '/boot/vmlinuz-4.18.0-305.3.1.el8_4.x86_64'},
    ]

    actual_entries = sorted(bootloader_config.entries, key=lambda entry: entry.title)

    for actual_entry, expected_entry in zip(actual_entries, expected_entries):
        assert actual_entry.title == expected_entry['title']
        assert actual_entry.kernel_image == expected_entry['kernel_image']


# repos/system_upgrade/el7toel8/actors/spamassassinconfigcheck/libraries/spamassassinconfigcheck.py

from leapp import reporting
from leapp.libraries.common.spamassassinutils import \
    SPAMC_CONFIG_FILE, SPAMASSASSIN_SERVICE_OVERRIDE, SYSCONFIG_SPAMASSASSIN


def _check_spamc_config(facts, report_func):
    title = ('spamc no longer allows specifying the TLS version and no longer '
             'supports SSLv3')
    summary_generic = ('spamc no longer allows using the "--ssl" option with an '
                       'argument specifying the TLS version - the option can only '
                       'be used without an argument. Also, spamc no longer supports '
                       'SSLv3.')
    if facts.spamc_ssl_argument:
        summary_detail = ('The spamc configuration file uses "--ssl %s", it will '
                          'be updated during the upgrade.'
                          % facts.spamc_ssl_argument)
        summary = reporting.Summary('%s %s' % (summary_generic, summary_detail))
        resource = reporting.RelatedResource('file', SPAMC_CONFIG_FILE)
    else:
        summary = reporting.Summary(summary_generic)
        resource = None
    severity = (reporting.Severity.HIGH if facts.spamc_ssl_argument == 'sslv3'
                else reporting.Severity.MEDIUM)
    hint = 'Please update your scripts and configuration, if there are any.'

    args = [
        reporting.Title(title),
        summary,
        reporting.Tags([reporting.Tags.ENCRYPTION]),
        reporting.Severity(severity),
        reporting.Remediation(hint=hint),
    ]
    if resource:
        args.append(resource)
    report_func(args)


def _check_spamd_config_ssl(facts, report_func):
    title = ('spamd no longer allows specifying the TLS version and no longer '
             'supports SSLv3')
    summary_generic = ('spamd no longer accepts the --ssl-version option and '
                       'no longer supports SSLv3.')
    if facts.spamd_ssl_version:
        summary_detail = ('The spamd sysconfig file uses "--ssl-version %s", '
                          'it will be updated during the upgrade.'
                          % facts.spamd_ssl_version)
        summary = reporting.Summary('%s %s' % (summary_generic, summary_detail))
        resource = reporting.RelatedResource('file', SYSCONFIG_SPAMASSASSIN)
    else:
        summary = reporting.Summary(summary_generic)
        resource = None
    severity = (reporting.Severity.HIGH if facts.spamd_ssl_version == 'sslv3'
                else reporting.Severity.MEDIUM)
    hint = 'Please update your scripts and configuration, if there are any.'

    args = [
        reporting.Title(title),
        summary,
        reporting.Tags([reporting.Tags.ENCRYPTION, reporting.Tags.SERVICES]),
        reporting.Severity(severity),
        reporting.Remediation(hint=hint)
    ]
    if resource:
        args.append(resource)
    report_func(args)


def _check_spamd_config_service_type(facts, report_func):
    title = 'The type of the spamassassin systemd service has changed'
    summary_generic = 'The type of spamassassin.service has been changed from "forking" to "simple".'
    if facts.service_overriden:
        summary_detail = 'However, the service appears to be overriden; no migration action will occur.'
        resource = reporting.RelatedResource('file', SPAMASSASSIN_SERVICE_OVERRIDE)
    else:
        summary_detail = 'The spamassassin sysconfig file will be updated.'
        resource = reporting.RelatedResource('file', SYSCONFIG_SPAMASSASSIN)
    report_func([
        reporting.Title(title),
        reporting.Summary('%s %s' % (summary_generic, summary_detail)),
        reporting.Tags([reporting.Tags.SERVICES]),
        reporting.Severity(reporting.Severity.MEDIUM),
        resource
    ])


def _report_sa_update_change(report_func):
    summary = ('sa-update no longer supports SHA1 validation of filtering rules, '
               'SHA256/SHA512 validation is done instead. This may affect you if '
               'you are using an alternative update channel (sa-update used with '
               'option --channel or --channelfile), or if you install filtering '
               'rule updates directly from files (sa-update --install).')
    report_func([reporting.Title('sa-update no longer supports SHA1 validation'),
                 reporting.Summary(summary),
                 reporting.Severity(reporting.Severity.LOW)])


def produce_reports(facts):
    """
    Checks spamassassin configuration and produces reports.
    """
    _check_spamc_config(facts, reporting.create_report)
    _check_spamd_config_ssl(facts, reporting.create_report)
    _check_spamd_config_service_type(facts, reporting.create_report)
    _report_sa_update_change(reporting.create_report)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigcheck/tests/test_actor_spamassassinconfigcheck.py

from leapp.models import SpamassassinFacts
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context


def test_actor_basic(current_actor_context):
    facts = SpamassassinFacts(service_overriden=False)

    current_actor_context.feed(facts)
    current_actor_context.run()
    reports = current_actor_context.consume(Report)

    assert len(reports) == 4
    report = reports[0]
    assert '--ssl' in report.report['summary']
    assert 'spamc' in report.report['summary']
    report = reports[1]
    assert '--ssl-version' in report.report['summary']
    assert 'spamd' in report.report['summary']
    report = reports[2]
    assert 'spamassassin.service' in report.report['summary']
    report = reports[3]
    assert 'sa-update no longer supports SHA1' in report.report['summary']


def test_actor_no_message(current_actor_context):
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigcheck/tests/test_library_spamassassinconfigcheck.py

from leapp.libraries.actor import spamassassinconfigcheck
from leapp.libraries.common.testutils import create_report_mocked
from leapp.models import SpamassassinFacts


def test_check_spamc_config_tlsv1():
    facts = SpamassassinFacts(spamc_ssl_argument='tlsv1', service_overriden=False)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamc_config(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'specifying the TLS version' in report_fields['title']
    assert 'SSLv3' in report_fields['title']
    assert '--ssl' in report_fields['summary']
    assert 'SSLv3' in report_fields['summary']
    assert 'spamc configuration file' in report_fields['summary']
    assert '--ssl tlsv1' in report_fields['summary']
    assert all('update your scripts' in r['remediations']['context']
               for r in report_fields['remediations'])
    assert report_fields['severity'] == 'medium'


def test_check_spamc_config_sslv3():
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamc_config(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'specifying the TLS version' in report_fields['title']
    assert 'SSLv3' in report_fields['title']
    assert '--ssl' in report_fields['summary']
    assert 'SSLv3' in report_fields['summary']
    assert 'spamc configuration file' in report_fields['summary']
    assert '--ssl sslv3' in report_fields['summary']
    assert all('update your scripts' in r['remediations']['context']
               for r in report_fields['remediations'])
    assert report_fields['severity'] == 'high'


def test_check_spamc_config_correct_config():
    facts = SpamassassinFacts(spamc_ssl_argument=None, service_overriden=False)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamc_config(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'specifying the TLS version' in report_fields['title']
    assert 'SSLv3' in report_fields['title']
    assert '--ssl' in report_fields['summary']
    assert 'SSLv3' in report_fields['summary']
    assert 'spamc configuration file' not in report_fields['summary']
    assert all('update your scripts' in r['remediations']['context']
               for r in report_fields['remediations'])
    assert report_fields['severity'] == 'medium'


def test_check_spamd_config_ssl_tlsv1():
    facts = SpamassassinFacts(spamd_ssl_version='tlsv1', service_overriden=False)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamd_config_ssl(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'specifying the TLS version' in report_fields['title']
    assert 'SSLv3' in report_fields['title']
    assert '--ssl-version' in report_fields['summary']
    assert 'SSLv3' in report_fields['summary']
    assert 'sysconfig' in report_fields['summary']
    assert '--ssl-version tlsv1' in report_fields['summary']
    assert all('update your scripts' in r['remediations']['context']
               for r in report_fields['remediations'])
    assert report_fields['severity'] == 'medium'


def test_check_spamd_config_ssl_sslv3():
    facts = SpamassassinFacts(spamd_ssl_version='sslv3', service_overriden=False)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamd_config_ssl(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'specifying the TLS version' in report_fields['title']
    assert 'SSLv3' in report_fields['title']
    assert '--ssl-version' in report_fields['summary']
    assert 'SSLv3' in report_fields['summary']
    assert 'sysconfig' in report_fields['summary']
    assert '--ssl-version sslv3' in report_fields['summary']
    assert all('update your scripts' in r['remediations']['context']
               for r in report_fields['remediations'])
    assert report_fields['severity'] == 'high'


def test_check_spamd_config_ssl_correct_config():
    facts = SpamassassinFacts(spamd_ssl_version=None, service_overriden=False)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamd_config_ssl(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'specifying the TLS version' in report_fields['title']
    assert 'SSLv3' in report_fields['title']
    assert '--ssl-version' in report_fields['summary']
    assert 'SSLv3' in report_fields['summary']
    assert 'sysconfig' not in report_fields['summary']
    assert all('update your scripts' in r['remediations']['context']
               for r in report_fields['remediations'])
    assert report_fields['severity'] == 'medium'


def test_check_spamd_config_service_type_service_overriden():
    facts = SpamassassinFacts(service_overriden=True)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamd_config_service_type(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'type of the spamassassin systemd service' in report_fields['title']
    assert 'The type of spamassassin.service' in report_fields['summary']
    assert 'overriden' in report_fields['summary']
    assert report_fields['severity'] == 'medium'


def test_check_spamd_config_service_type_service_not_overriden():
    facts = SpamassassinFacts(service_overriden=False)
    report_func = create_report_mocked()

    spamassassinconfigcheck._check_spamd_config_service_type(facts, report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'type of the spamassassin systemd service' in report_fields['title']
    assert 'The type of spamassassin.service' in report_fields['summary']
    assert 'will be updated' in report_fields['summary']
    assert report_fields['severity'] == 'medium'


def test_report_sa_update_change():
    report_func = create_report_mocked()

    spamassassinconfigcheck._report_sa_update_change(report_func)

    assert report_func.called == 1
    report_fields = report_func.report_fields
    assert 'sa-update no longer supports SHA1' in report_fields['title']
    assert 'no longer supports SHA1' in report_fields['summary']
    assert 'SHA256/SHA512' in report_fields['summary']
    assert '--channel or --channelfile' in report_fields['summary']
    assert '--install' in report_fields['summary']
    assert report_fields['severity'] == 'low'


# repos/system_upgrade/el7toel8/actors/spamassassinconfigread/libraries/spamassassinconfigread.py

from leapp.libraries.actor import spamassassinconfigread_spamc, spamassassinconfigread_spamd
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM, SpamassassinFacts


def is_processable():
    """
    Checks whether the spamassassin package is installed.
    """
    res = has_package(InstalledRedHatSignedRPM, 'spamassassin')
    if not res:
        api.current_logger().debug('spamassassin is not installed.')
    return res


def get_spamassassin_facts(read_func, listdir):
    """
    Reads the spamc configuration file, the spamassassin sysconfig file and checks
    whether the spamassassin service is overriden. Returns SpamassassinFacts.
    """
    spamc_ssl_argument = spamassassinconfigread_spamc.get_spamc_ssl_argument(read_func)
    service_overriden = spamassassinconfigread_spamd.spamassassin_service_overriden(listdir)
    spamd_ssl_version = spamassassinconfigread_spamd.get_spamd_ssl_version(read_func)
    return SpamassassinFacts(spamc_ssl_argument=spamc_ssl_argument,
                             service_overriden=service_overriden,
                             spamd_ssl_version=spamd_ssl_version)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigread/libraries/spamassassinconfigread_spamc.py

import errno
import re

from leapp.libraries.common.spamassassinutils import SPAMC_CONFIG_FILE
from leapp.libraries.stdlib import api


def _remove_comments(content):
    return re.sub(r'^#.*$', '', content, flags=re.MULTILINE)


def _parse_spamc_ssl_argument(content):
    content = _remove_comments(content)
    res = None
    for match in re.finditer(r'(?<!\S)--ssl(\s+|=)(sslv3|tlsv1)(?!\S)', content):
        arg = match.group(2)
        if arg == 'tlsv1' or (arg == 'sslv3' and res is None):
            res = arg
    return res


def get_spamc_ssl_argument(read_func):
    """
    Extracts and returns the argument given to the --ssl option from the spamc
    configuration file. Returns None if the option is not specified or the config
    file does not exist.
    """
    try:
        content = read_func(SPAMC_CONFIG_FILE)
        return _parse_spamc_ssl_argument(content)
    except (IOError, OSError) as e:
        if e.errno != errno.ENOENT:
            api.current_logger().warning(
                'Failed to read spamc configuration file: %s' % e)
        return None


# repos/system_upgrade/el7toel8/actors/spamassassinconfigread/libraries/spamassassinconfigread_spamd.py

import errno
import os
import re

import leapp.libraries.common.spamassassinutils as lib
from leapp.libraries.stdlib import api


def spamassassin_service_overriden(listdir):
    file_name = os.path.basename(lib.SPAMASSASSIN_SERVICE_OVERRIDE)
    dir_path = os.path.dirname(lib.SPAMASSASSIN_SERVICE_OVERRIDE)
    try:
        return file_name in listdir(dir_path)
    except OSError as e:
        return e.errno != errno.ENOENT


def _parse_ssl_version(content):
    _, assignment, _ = lib.parse_sysconfig_spamassassin(content)
    if re.search(r'(?<![\w-])--ssl-version(=|\s+)sslv3(?![\w-])', assignment):
        return 'sslv3'
    if re.search(r'(?<![\w-])--ssl-version(=|\s+)tlsv1(?![\w-])', assignment):
        return 'tlsv1'
    return None


def get_spamd_ssl_version(read_func):
    """
    Extracts and returns the argument given to the --ssl-version option from
    the spamassassin sysconfig file. Returns None if the option is not specified
    or the file does not exist.
    """
    try:
        content = read_func(lib.SYSCONFIG_SPAMASSASSIN)
    except (IOError, OSError) as e:
        if e.errno != errno.ENOENT:
            api.current_logger().warning(
                'Failed to read spamassassin sysconfig file: %s' % e)
        return None
    return _parse_ssl_version(content)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigread/tests/test_lib_spamc_spamassassinconfigread.py

import errno

from leapp.libraries.actor import spamassassinconfigread_spamc
from leapp.libraries.common.testutils import make_IOError


class MockFileOperations(object):
    def __init__(self, to_raise=None):
        self.files = {}
        self.files_read = {}
        self.read_called = 0
        self.to_raise = to_raise

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        if self.to_raise is not None:
            raise self.to_raise
        try:
            return self.files[path]
        except KeyError:
            raise make_IOError(errno.ENOENT)


def test_parse_spamc_ssl_argument():
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl sslv3')
    assert value == 'sslv3'

    # equal sign format
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl=tlsv1')
    assert value == 'tlsv1'


def test_parse_spamc_ssl_argument_without_valid_argument():
    # unknown argument
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl foo')
    assert value is None

    # --ssl followed by another option
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl -B')
    assert value is None

    # space surrounding the equal sign. This amounts to an unrecognized
    # argument (empty string)
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl= tlsv1')
    assert value is None

    # space surrounding the equal sign. This amounts to an unrecognized
    # argument ("=tlsv1")
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl =tlsv1')
    assert value is None


def test_parse_spamc_ssl_argument_multiline():
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('-B --ssl \n sslv3 -c\n-H')
    assert value == 'sslv3'


def test_parse_spamc_ssl_argument_tls_supersedes_ssl():
    # Due to the way the spamc cmdline parser works, if '--ssl tlsv1' is
    # specified, all other --ssl options are effectively ignored and tlsv1 is
    # used.
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl sslv3 --ssl tlsv1')
    assert value == 'tlsv1'

    # reverse order
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl tlsv1 --ssl sslv3')
    assert value == 'tlsv1'


def test_parse_spamc_ssl_argument_comment():
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('# --ssl tlsv1')
    assert value is None

    # comment mixed with actual option
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('# --ssl tlsv1\n--ssl sslv3')
    assert value == 'sslv3'

    # comment mixed with actual option
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl tlsv1\n# --ssl sslv3')
    assert value == 'tlsv1'


def test_parse_spamc_ssl_argument_crazy_corner_cases():
    # The option and value can have a comment in between
    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl\n# foo\ntlsv1')
    assert value == 'tlsv1'

    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl\n# foo\n# bar\nsslv3')
    assert value == 'sslv3'

    value = spamassassinconfigread_spamc._parse_spamc_ssl_argument('--ssl\n# foo\n# tlsv1\nsslv3')
    assert value == 'sslv3'


def test_get_spamc_ssl_argument():
    path = '/etc/mail/spamassassin/spamc.conf'
    fileops = MockFileOperations()
    fileops.files[path] = '--ssl sslv3'

    value = spamassassinconfigread_spamc.get_spamc_ssl_argument(fileops.read)

    assert fileops.files_read == {path: 1}
    assert value == 'sslv3'


def test_get_spamc_ssl_argument_empty():
    path = '/etc/mail/spamassassin/spamc.conf'
    fileops = MockFileOperations()
    fileops.files[path] = ''

    value = spamassassinconfigread_spamc.get_spamc_ssl_argument(fileops.read)

    assert fileops.files_read == {path: 1}
    assert value is None


def test_get_spamc_ssl_argument_nonexistent():
    path = '/etc/mail/spamassassin/spamc.conf'
    fileops = MockFileOperations()

    value = spamassassinconfigread_spamc.get_spamc_ssl_argument(fileops.read)

    assert fileops.files_read == {path: 1}
    assert value is None


def test_get_spamc_ssl_argument_inaccessible():
    path = '/etc/mail/spamassassin/spamc.conf'
    fileops = MockFileOperations(to_raise=make_IOError(errno.EACCES))

    value = spamassassinconfigread_spamc.get_spamc_ssl_argument(fileops.read)

    assert fileops.files_read == {path: 1}
    assert value is None


# repos/system_upgrade/el7toel8/actors/spamassassinconfigread/tests/test_lib_spamd_spamassassinconfigread.py

import errno
import os

from leapp.libraries.actor import spamassassinconfigread_spamd
from leapp.libraries.common.testutils import make_IOError, make_OSError


class MockFileOperations(object):
    def __init__(self, to_raise=None):
        self.files = {}
        self.files_read = {}
        self.read_called = 0
        self.to_raise = to_raise

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        if self.to_raise is not None:
            raise self.to_raise
        try:
            return self.files[path]
        except KeyError:
            raise make_IOError(errno.ENOENT)


class MockListDir(object):
    def __init__(self, path=None, file_names=None, to_raise=None):
        self.path = None if path is None else os.path.normpath(path)
        self.file_names = file_names
        self.to_raise = to_raise
        self.error = False

    def listdir(self, path):
        if self.to_raise:
            raise self.to_raise
        if os.path.normpath(path) == self.path:
            return self.file_names
        self.error = True
        raise make_OSError(errno.ENOENT)


def test_spamassassin_service_overriden():
    listdir = MockListDir(path='/etc/systemd/system', file_names=['spamassassin.service'])
    overriden = spamassassinconfigread_spamd.spamassassin_service_overriden(listdir.listdir)
    assert overriden is True

    listdir = MockListDir(path='/etc/systemd/system',
                          file_names=['foo.service', 'spamassassin.service', 'bar.service'])
    overriden = spamassassinconfigread_spamd.spamassassin_service_overriden(listdir.listdir)
    assert overriden is True
    assert not listdir.error


def test_spamassassin_service_overriden_nonexistent():
    listdir = MockListDir(path='/etc/systemd/system', file_names=[])
    overriden = spamassassinconfigread_spamd.spamassassin_service_overriden(listdir.listdir)
    assert overriden is False

    listdir = MockListDir(path='/etc/systemd/system',
                          file_names=['foo.service', 'bar.service'])
    overriden = spamassassinconfigread_spamd.spamassassin_service_overriden(listdir.listdir)
    assert overriden is False
    assert not listdir.error


def test_spamassassin_service_overriden_nonexistent_dir():
    listdir = MockListDir(to_raise=make_OSError(errno.ENOENT))
    overriden = spamassassinconfigread_spamd.spamassassin_service_overriden(listdir.listdir)
    assert overriden is False


def test_spamassassin_service_overriden_nonexistent_inaccessible():
    # If we can't check if the file is there, we treat it as if it was there,
    # so that the SpamassassinConfigUpdate actor doesn't make changes to
    # /etc/sysconfig/spamassassin that may not be justified.
    listdir = MockListDir(to_raise=make_OSError(errno.EACCES))
    overriden = spamassassinconfigread_spamd.spamassassin_service_overriden(listdir.listdir)
    assert overriden is True


def test_parse_ssl_version_sslv3():
    content = 'SPAMDOPTIONS="--ssl-version sslv3"'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'sslv3'

    content = 'SPAMDOPTIONS="-cd -m5 --ssl-version sslv3 -H"'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'sslv3'


def test_parse_ssl_version_tlsv1():
    content = 'SPAMDOPTIONS="--ssl-version tlsv1"'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'tlsv1'

    content = 'SPAMDOPTIONS="-cd -m5 --ssl-version tlsv1 -H"'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'tlsv1'


def test_parse_ssl_version_invalid_argument():
    # If an invalid argument is used, we treat it as if the option was not
    # specified at all, so that the config update actor doesn't touch it. We
    # don't want to break the config even more.
    content = 'SPAMDOPTIONS="--ssl-version foo"\n'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value is None


def test_parse_ssl_version_comments():
    content = '# foo\nSPAMDOPTIONS="--ssl-version tlsv1"\n# bar\n'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'tlsv1'


def test_parse_ssl_version_repeated():
    # The last --ssl-version option takes effect
    content = 'SPAMDOPTIONS="--ssl-version tlsv1 --ssl-version sslv3"\n'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'sslv3'

    content = 'SPAMDOPTIONS="--ssl-version sslv3 --ssl-version tlsv1"\n'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'sslv3'


def test_parse_ssl_version_last_assignment_takes_effect():
    # The last assignment to SPAMDOPTIONS takes effect
    content = 'SPAMDOPTIONS="--ssl-version tlsv1"\nSPAMDOPTIONS="--ssl-version sslv3"\n'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'sslv3'


def test_parse_ssl_version_multiline():
    content = 'SPAMDOPTIONS="--ssl \\\n --ssl-version tlsv1"\n'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'tlsv1'

    content = 'SPAMDOPTIONS="--ssl-version \\\n sslv3"\n'
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'sslv3'


def test_parse_ssl_version_multiline_comment():
    content = ('SPAMDOPTIONS="--ssl-version tlsv1"\n'
               '# foo \\\nSPAMDOPTIONS="--ssl-version sslv3" \\\n still a comment')
    value = spamassassinconfigread_spamd._parse_ssl_version(content)
    assert value == 'tlsv1'


def test_get_spamd_ssl_version():
    path = '/etc/sysconfig/spamassassin'
    fileops = MockFileOperations()
    fileops.files[path] = '# foo\nSPAMDOPTIONS="--ssl-version tlsv1"\n# bar\n'

    value = spamassassinconfigread_spamd.get_spamd_ssl_version(fileops.read)

    assert value == 'tlsv1'


def test_get_spamd_ssl_version_nonexistent():
    fileops = MockFileOperations()
    value = spamassassinconfigread_spamd.get_spamd_ssl_version(fileops.read)
    assert value is None


def test_get_spamd_ssl_version_inaccessible():
    fileops = MockFileOperations(to_raise=make_IOError(errno.EACCES))
    value = spamassassinconfigread_spamd.get_spamd_ssl_version(fileops.read)
    assert value is None


# repos/system_upgrade/el7toel8/actors/spamassassinconfigread/tests/test_library_spamassassinconfigread.py

import errno
import os

from leapp.libraries.actor import spamassassinconfigread
from leapp.libraries.common.testutils import make_IOError, make_OSError


class MockFileOperations(object):
    def __init__(self):
        self.files = {}
        self.files_read = {}
        self.read_called = 0

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        try:
            return self.files[path]
        except KeyError:
            raise make_IOError(errno.ENOENT)


class MockListDir(object):
    def __init__(self, path=None, file_names=None):
        self.path = None if path is None else os.path.normpath(path)
        self.file_names = file_names
        self.error = False

    def listdir(self, path):
        if os.path.normpath(path) == self.path:
            return self.file_names
        self.error = True
        raise make_OSError(errno.ENOENT)


def test_get_spamassassin_facts():
    spamc_path = '/etc/mail/spamassassin/spamc.conf'
    spamd_path = '/etc/sysconfig/spamassassin'
    fileops = MockFileOperations()
    fileops.files[spamc_path] = '--ssl sslv3'
    fileops.files[spamd_path] = 'SPAMDOPTIONS="--ssl-version tlsv1"'
    listdir = MockListDir(path='/etc/systemd/system', file_names=['spamassassin.service'])

    facts = spamassassinconfigread.get_spamassassin_facts(read_func=fileops.read, listdir=listdir.listdir)

    assert len(fileops.files_read) == 2
    assert spamc_path in fileops.files_read
    assert spamd_path in fileops.files_read
    assert facts.spamc_ssl_argument == 'sslv3'
    assert facts.spamd_ssl_version == 'tlsv1'
    assert facts.service_overriden is True
    assert not listdir.error


def test_get_spamassassin_facts_nonexistent_config():
    spamc_path = '/etc/mail/spamassassin/spamc.conf'
    spamd_path = '/etc/sysconfig/spamassassin'
    fileops = MockFileOperations()
    listdir = MockListDir(path='/etc/systemd/system', file_names=[])

    facts = spamassassinconfigread.get_spamassassin_facts(read_func=fileops.read, listdir=listdir.listdir)

    assert len(fileops.files_read) == 2
    assert spamc_path in fileops.files_read
    assert spamd_path in fileops.files_read
    assert facts.spamc_ssl_argument is None
    assert facts.spamd_ssl_version is None
    assert facts.service_overriden is False
    assert not listdir.error


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/libraries/spamassassinconfigupdate.py

from leapp.libraries.actor import (
    spamassassinconfigupdate_backup,
    spamassassinconfigupdate_spamc,
    spamassassinconfigupdate_spamd
)


class FileOperations(object):
    def read(self, path):
        with open(path, 'r') as f:
            return f.read()

    def write(self, path, content):
        with open(path, 'w') as f:
            f.write(content)


def migrate_configs(facts, fileops=FileOperations(),
                    backup_func=spamassassinconfigupdate_backup.backup_file):
    """
    Perform necessary changes in spamassassin configuration. See
    lib_spamc.migrate_spamc_config() and lib_spamd.migrate_spamd_config for details.
    """
    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)
    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/libraries/spamassassinconfigupdate_backup.py

import errno
import os
import shutil
import tempfile


def backup_file(path):
    """ Backup the file specified by path and return the path to the backup file. """
    backup_path = '%s.leapp-backup' % path
    try:
        fd = os.open(backup_path, os.O_CREAT | os.O_EXCL, 0o600)
    except OSError as e:
        if e.errno == errno.EEXIST:
            file_name = os.path.basename(backup_path)
            fd, backup_path = tempfile.mkstemp(prefix=file_name + '.',
                                               dir=os.path.dirname(backup_path))
        else:
            raise
    os.close(fd)

    shutil.copyfile(path, backup_path)
    return backup_path


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/libraries/spamassassinconfigupdate_spamc.py

import re

from leapp.libraries.common.spamassassinutils import SPAMC_CONFIG_FILE
from leapp.libraries.stdlib import api


def _rewrite_spamc_config(content):
    res_lines = []
    read_ssl_option = False
    for line in content.split('\n'):
        if line.startswith('#'):
            res_lines.append(line)
            continue
        if read_ssl_option:
            line = re.sub(r'^\s*(sslv3|tlsv1)', '', line)
            read_ssl_option = False
        if re.search(r'(?<!\S)--ssl\s*$', line):
            read_ssl_option = True
        line = re.sub(r'(?<!\S)--ssl(\s+|=)(sslv3|tlsv1)(?!\S)', '--ssl', line)
        res_lines.append(line)
    res = '\n'.join(res_lines)
    return res


def migrate_spamc_config(facts, fileops, backup_func):
    """
    Removes arguments given to the --ssl option in the spamc config file.
    The file is backed up beforehand.
    """
    if facts.spamc_ssl_argument is None:
        api.current_logger().info('There is nothing to migrate in the spamc configuration file')
        return
    try:
        backup_path = backup_func(SPAMC_CONFIG_FILE)
        api.current_logger().info('spamc configuration file backup created at %s.'
                                  % backup_path)
    except (OSError, IOError) as e:
        api.current_logger().warning(
            'spamc configuration file migration will not be performed. Failed to create backup: %s' % e)
        return
    try:
        content = fileops.read(SPAMC_CONFIG_FILE)
        new_content = _rewrite_spamc_config(content)
        fileops.write(SPAMC_CONFIG_FILE, new_content)
    except (OSError, IOError) as e:
        api.current_logger().warning('Failed to migrate spamc configuration file: %s' % e)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/libraries/spamassassinconfigupdate_spamd.py

import errno
import re

import leapp.libraries.common.spamassassinutils as utils
from leapp.libraries.stdlib import api


def _drop_ssl_version(value):
    regex = r'(?<![\w-])--ssl-version(=|\s+)(sslv3|tlsv1)(?![\w-])'
    if not re.search(r'(?<![\w-])--ssl(?![\w-])', value):
        value = re.sub(regex, '--ssl', value, count=1)
    return re.sub(regex, '', value)


def _drop_daemonize_option(value):
    shortopts = utils.SPAMD_SHORTOPTS_NOARG
    value = re.sub(r'(?<![\w-])-d+(?![\w-])',
                   r'', value)
    while True:
        value, nsubs = re.subn(r'(?<![\w-])(-[' + shortopts + ']*)d',
                               r'\1', value)
        if nsubs == 0:
            break
    return re.sub(r'(?<![\w-])--daemonize(?![\w-])', '', value)


def _rewrite_spamd_option(content, ops):
    pre_assignment, assignment, post_assignment = \
        utils.parse_sysconfig_spamassassin(content)
    if assignment:
        value = re.sub(r'^\s*' + utils.SYSCONFIG_VARIABLE + r'\s*=(.*)$',
                       r'\1', assignment)
        for op in ops:
            value = op(value)
        assignment = utils.SYSCONFIG_VARIABLE + '=' + value
    res = '\n'.join(filter(None, (pre_assignment, assignment, post_assignment)))
    if res and not res.endswith('\n'):
        res += '\n'
    return res


def _rewrite_spamd_config(facts, content):
    ops = []
    if facts.spamd_ssl_version:
        ops.append(_drop_ssl_version)
    if not facts.service_overriden:
        ops.append(_drop_daemonize_option)
    return _rewrite_spamd_option(content, ops) if ops else content


def migrate_spamd_config(facts, fileops, backup_func):
    """
    Removes --ssl-version and -d/--daemonize options from the spamassassin
    sysconfig file. The file is backed up beforehand.
    """
    nothing_to_migrate_msg = 'There is nothing to migrate in the spamd configuration file'
    if facts.service_overriden and not facts.spamd_ssl_version:
        api.current_logger().info(nothing_to_migrate_msg)
        return
    try:
        content = fileops.read(utils.SYSCONFIG_SPAMASSASSIN)
    except (OSError, IOError) as e:
        if e.errno == errno.ENOENT:
            api.current_logger().info(nothing_to_migrate_msg)
        else:
            api.current_logger().warning('Failed to read spamd configuration file: %s' % e)
        return
    new_content = _rewrite_spamd_config(facts, content)
    if new_content == content:
        api.current_logger().info('There is nothing to migrate in the spamd configuration file')
        return
    try:
        backup_path = backup_func(utils.SYSCONFIG_SPAMASSASSIN)
        api.current_logger().info('spamd configuration file backup created at %s.'
                                  % backup_path)
    except (OSError, IOError) as e:
        api.current_logger().warning(
            'spamd configuration file migration will not be performed. Failed to create backup: %s' % e)
        return
    try:
        fileops.write(utils.SYSCONFIG_SPAMASSASSIN, new_content)
    except (OSError, IOError) as e:
        api.current_logger().warning('Failed to rewrite the spamd configuration file: %s' % e)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/tests/test_lib_backup_spamassassinconfigupdate.py

import os
import shutil
import stat
import tempfile

from leapp.libraries.actor.spamassassinconfigupdate_backup import backup_file


def test_backup_file():
    tmpdir = tempfile.mkdtemp()
    try:
        file_path = os.path.join(tmpdir, 'foo-bar')
        content = 'test content\n'
        with open(file_path, 'w') as f:
            f.write(content)

        backup_path = backup_file(file_path)

        assert os.path.basename(backup_path) == 'foo-bar.leapp-backup'
        assert os.path.dirname(backup_path) == tmpdir
        assert len(os.listdir(tmpdir)) == 2
        st = os.stat(backup_path)
        assert stat.S_IMODE(st.st_mode) == (stat.S_IRUSR | stat.S_IWUSR)
        with open(backup_path, 'r') as f:
            backup_content = f.read()
        assert backup_content == content
        with open(file_path, 'r') as f:
            orig_content = f.read()
        assert orig_content == content
    finally:
        shutil.rmtree(tmpdir)


def test_backup_file_target_exists():
    tmpdir = tempfile.mkdtemp()
    try:
        file_path = os.path.join(tmpdir, 'foo-bar')
        primary_target_path = '%s.leapp-backup' % file_path
        primary_target_content = 'do not overwrite me'
        content = 'test_content\n'
        with open(file_path, 'w') as f:
            f.write(content)
        with open(primary_target_path, 'w') as f:
            f.write(primary_target_content)

        backup_path = backup_file(file_path)

        assert os.path.basename(backup_path).startswith('foo-bar.leapp-backup.')
        assert os.path.dirname(backup_path) == tmpdir
        assert len(os.listdir(tmpdir)) == 3
        st = os.stat(backup_path)
        assert stat.S_IMODE(st.st_mode) == (stat.S_IRUSR | stat.S_IWUSR)
        with open(backup_path, 'r') as f:
            assert f.read() == content
        with open(primary_target_path, 'r') as f:
            assert f.read() == primary_target_content
    finally:
        shutil.rmtree(tmpdir)


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/tests/test_lib_spamc_spamassassinconfigupdate.py

import errno

from leapp.libraries.actor import spamassassinconfigupdate_spamc
from leapp.libraries.common.spamassassinutils import SPAMC_CONFIG_FILE
from leapp.libraries.common.testutils import make_IOError, make_OSError
from leapp.models.spamassassinfacts import SpamassassinFacts


# The test cases reuse values from the SpamassassinConfigRead test cases


def test_rewrite_spamc_config():
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl sslv3\n')
    assert new_content == '--ssl\n'

    # equal sign format
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl=tlsv1\n')
    assert new_content == '--ssl\n'

    # no argument
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl\n')
    assert new_content == '--ssl\n'


def test_rewrite_spamc_config_without_valid_argument():
    # If we encounter an unrecognized parameter, we leave it be - the
    # configuration is invalid anyway, so let's not mess it up even more.
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl foo\n')
    assert new_content == '--ssl foo\n'

    # --ssl followed by another option
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl -B\n')
    assert new_content == '--ssl -B\n'

    # space surrounding the equal sign. This amounts to an unrecognized
    # argument (empty string)
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl= tlsv1\n')
    assert new_content == '--ssl= tlsv1\n'

    # space surrounding the equal sign. This amounts to an unrecognized
    # argument ("=tlsv1")
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl =tlsv1\n')
    assert new_content == '--ssl =tlsv1\n'


def test_rewrite_spamc_config_multiline():
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('-B --ssl \n sslv3 -c\n-H\n')
    assert new_content == '-B --ssl \n -c\n-H\n'

    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl\n-B\n')
    assert new_content == '--ssl\n-B\n'


def test_rewrite_spamc_config_tls_supersedes_ssl():
    # Ideally, the result would be a single '--ssl' option. However for
    # simplicity, we allow the option to be output twice. It's not nice, but
    # there's nothing technically wrong with it. And it's really a corner case
    # anyway.  If someone fixes it, this test case can be updated to expect
    # '--ssl' as output.
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl sslv3 --ssl tlsv1\n')
    assert new_content == '--ssl --ssl\n'

    # reverse order
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl tlsv1 --ssl sslv3\n')
    assert new_content == '--ssl --ssl\n'


def test_rewrite_spamc_config_comment():
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('# --ssl tlsv1\n')
    assert new_content == '# --ssl tlsv1\n'

    # comment mixed with actual option
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('# --ssl tlsv1\n--ssl sslv3\n')
    assert new_content == '# --ssl tlsv1\n--ssl\n'

    # comment mixed with actual option
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl tlsv1\n# --ssl sslv3\n')
    assert new_content == '--ssl\n# --ssl sslv3\n'


def test_rewrite_spamc_config_crazy_corner_cases():
    # The option and new_content can have a comment in between
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl\n# foo\ntlsv1\n')
    assert new_content == '--ssl\n# foo\n\n'

    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl\n# foo\n# bar\nsslv3\n')
    assert new_content == '--ssl\n# foo\n# bar\n\n'

    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl\n# foo\n# tlsv1\nsslv3\n')
    assert new_content == '--ssl\n# foo\n# tlsv1\n\n'

    # --ssl followed by another option
    new_content = spamassassinconfigupdate_spamc._rewrite_spamc_config('--ssl\n# foo\n-B\n')
    assert new_content == '--ssl\n# foo\n-B\n'


class MockBackup(object):
    def __init__(self, to_raise=None):
        self.to_raise = to_raise
        self.called = 0
        self.paths = []

    def __call__(self, path):
        self.called += 1
        self.paths.append(path)
        if self.to_raise:
            raise self.to_raise
        return '/path/to/backup'


class MockFileOperations(object):
    def __init__(self, read_error=None, write_error=None):
        self.files = {}
        self.files_read = {}
        self.files_written = {}
        self.read_called = 0
        self.write_called = 0
        self.read_error = read_error
        self.write_error = write_error

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        if self.read_error:
            raise self.read_error
        return self.files[path]

    def _increment_write_counters(self, path):
        self.write_called += 1
        self.files_written.setdefault(path, 0)
        self.files_written[path] += 1

    def write(self, path, content):
        self._increment_write_counters(path)
        if self.write_error:
            raise self.write_error
        self.files[path] = content


def test_migrate_spamc_config():
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    fileops = MockFileOperations()
    fileops.files[SPAMC_CONFIG_FILE] = '--ssl sslv3\n# foo\n-B\n'
    backup_func = MockBackup()

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    assert backup_func.called == 1
    assert backup_func.paths[0] == SPAMC_CONFIG_FILE
    assert fileops.files[SPAMC_CONFIG_FILE] == '--ssl\n# foo\n-B\n'
    assert fileops.read_called == 1
    assert fileops.files_read[SPAMC_CONFIG_FILE] == 1
    assert fileops.write_called == 1
    assert fileops.files_written[SPAMC_CONFIG_FILE] == 1


def test_migrate_spamc_config_no_ssl_option():
    facts = SpamassassinFacts(spamc_ssl_argument=None, service_overriden=False)
    fileops = MockFileOperations()
    backup_func = MockBackup()

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    assert backup_func.called == 0
    assert fileops.read_called == 0
    assert fileops.write_called == 0


def test_migrate_spamc_config_no_write_if_backup_fails():
    # OSError (e.g. os.open)
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    fileops = MockFileOperations()
    backup_func = MockBackup(to_raise=make_OSError(errno.EACCES))

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    assert fileops.write_called == 0

    # IOError (e.g. file.read)
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    fileops = MockFileOperations()
    backup_func = MockBackup(to_raise=make_IOError(errno.EACCES))

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    assert fileops.write_called == 0


def test_migrate_spamc_config_read_failure():
    # OSError (e.g. os.open)
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    fileops = MockFileOperations(read_error=make_OSError(errno.EACCES))
    backup_func = MockBackup()

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    # The main purpose of this test is to check that exceptions are handled
    # properly. The following assertions are supplementary.
    assert fileops.read_called == 1
    assert fileops.write_called == 0

    # IOError (e.g. builtin open)
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    fileops = MockFileOperations(read_error=make_IOError(errno.EACCES))
    backup_func = MockBackup()

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    assert fileops.read_called == 1
    assert fileops.write_called == 0


def test_migrate_spamc_config_write_failure():
    # OSError (e.g. os.open)
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    fileops = MockFileOperations(write_error=make_OSError(errno.EACCES))
    fileops.files[SPAMC_CONFIG_FILE] = '--ssl sslv3\n# foo\n-B\n'
    backup_func = MockBackup()

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    # The main purpose of this test is to check that exceptions are handled
    # properly. The following assertions are supplementary.
    assert fileops.read_called == 1
    assert fileops.write_called == 1

    # IOError (e.g. builtin open)
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3', service_overriden=False)
    fileops = MockFileOperations(write_error=make_IOError(errno.EACCES))
    fileops.files[SPAMC_CONFIG_FILE] = '--ssl sslv3\n# foo\n-B\n'
    backup_func = MockBackup()

    spamassassinconfigupdate_spamc.migrate_spamc_config(facts, fileops, backup_func)

    assert fileops.read_called == 1
    assert fileops.write_called == 1


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/tests/test_lib_spamd_spamassassinconfigupdate.py

import errno

from leapp.libraries.actor import spamassassinconfigupdate_spamd
from leapp.libraries.common.spamassassinutils import SYSCONFIG_SPAMASSASSIN, SYSCONFIG_VARIABLE
from leapp.libraries.common.testutils import make_IOError, make_OSError
from leapp.models import SpamassassinFacts


# The tests for _drop_ssl_version and _drop_daemonize_option are overly
# restrictive in what output they accept - namely regarding whitespace and
# order of the options; don't be afraid to change the tests if they start
# failing due to whitespace or the order of options. It's done this way for
# simplicity - regular expressions accepting all acceptable outputs would be
# too complex.


def test_drop_ssl_version_double_quotes():
    value = '"--ssl --ssl-version tlsv1"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"--ssl "'

    value = '"--ssl --ssl-version=tlsv1"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"--ssl "'

    value = '"--ssl --ssl-version sslv3"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"--ssl "'

    value = '"-d -c -m5 -H --ssl --ssl-version tlsv1"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"-d -c -m5 -H --ssl "'

    value = '"-d -c -m5 -H --ssl --ssl-version sslv3"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"-d -c -m5 -H --ssl "'

    value = '"-d --ssl -c -m5 --ssl-version tlsv1 -H"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"-d --ssl -c -m5  -H"'

    value = '"-d --ssl -c -m5 --ssl-version sslv3 -H"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"-d --ssl -c -m5  -H"'

    value = '"-d --ssl -c -m5 --ssl-version=sslv3 -H"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"-d --ssl -c -m5  -H"'


def test_drop_ssl_version_no_quotes():
    value = '-d --ssl -c -m5 --ssl-version sslv3 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '-d --ssl -c -m5  -H'

    value = '-d --ssl -c -m5 --ssl-version=sslv3 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '-d --ssl -c -m5  -H'


def test_drop_ssl_version_single_quotes():
    value = "'-d --ssl -c -m5 --ssl-version sslv3 -H'"
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == "'-d --ssl -c -m5  -H'"

    value = "'-d --ssl -c -m5 --ssl-version=sslv3 -H'"
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == "'-d --ssl -c -m5  -H'"


def test_drop_ssl_version_ssl_version_implies_ssl():
    # If --ssl-version is used and --ssl is missing, we need to add --ssl
    # because --ssl-version implies --ssl
    value = '--ssl-version tlsv1'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '--ssl'

    value = '--ssl-version=tlsv1'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '--ssl'

    value = '"--ssl-version tlsv1"'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '"--ssl"'

    value = ' --ssl-version tlsv1 '
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == ' --ssl '

    value = '-d -c -m5 --ssl-version sslv3 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '-d -c -m5 --ssl -H'

    value = '-d -c -m5 --ssl-version=sslv3 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '-d -c -m5 --ssl -H'

    value = '-d -c -m5 -H --ssl-version sslv3'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == '-d -c -m5 -H --ssl'


def test_drop_ssl_version_invalid_argument():
    # If the argument of --ssl-version is invalid, we don't touch it because
    # the configuration is invalid and the user needs to fix it up anyway.
    value = '--ssl-version foo'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == value

    value = '--ssl-version=foo'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == value

    value = '-d --ssl -c --ssl-version foo -H'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == value

    value = '-d --ssl -c --ssl-version=foo -H'
    rewritten = spamassassinconfigupdate_spamd._drop_ssl_version(value)
    assert rewritten == value


def test_drop_daemonize_option_double_quotes():
    value = '"-d -c -m5 -H"'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == '" -c -m5 -H"'

    value = '"-c -d -m5 -H"'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == '"-c  -m5 -H"'


def test_drop_daemonize_option_no_quotes():
    value = '-d -c -m5 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == ' -c -m5 -H'

    value = '-c -d -m5 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == '-c  -m5 -H'


def test_drop_daemonize_option_single_quotes():
    value = "'-d -c -m5 -H'"
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == "' -c -m5 -H'"

    value = "'-c -d -m5 -H'"
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == "'-c  -m5 -H'"


def test_drop_daemonize_option_longopt():
    value = '--daemonize -c -m5 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == ' -c -m5 -H'

    value = '-c --daemonize -m5 -H'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == '-c  -m5 -H'


def test_drop_daemonize_short_form():
    value = '-cdL -m5'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == '-cL -m5'

    value = '-dcL -m5'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == '-cL -m5'

    value = '-cLd -m5'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == '-cL -m5'


def test_drop_daemonize_option_repeated():
    value = '--daemonize -cdd -m5 -dcd --daemonize -H -ddd'
    rewritten = spamassassinconfigupdate_spamd._drop_daemonize_option(value)
    assert rewritten == ' -c -m5 -c  -H '


def test_rewrite_spamd_option_no_assignment():
    content = '# foo\nbar=foobar\n'
    ops = [spamassassinconfigupdate_spamd._drop_ssl_version, spamassassinconfigupdate_spamd._drop_daemonize_option]
    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_option(content, ops)
    assert rewritten == content


def test_rewrite_spamd_option_comments():
    ops = [spamassassinconfigupdate_spamd._drop_ssl_version, spamassassinconfigupdate_spamd._drop_daemonize_option]

    # Leave comments be
    content = '# foo\n# bar\nSPAMDOPTIONS="--ssl-version tlsv1 -d --ssl"\n# foobar\n'
    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_option(content, ops)
    assert rewritten == '# foo\n# bar\nSPAMDOPTIONS="  --ssl"\n# foobar\n'

    content = '# foo\n# bar\n# SPAMDOPTIONS="--ssl-version tlsv1 -d --ssl"\n# foobar\n'
    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_option(content, ops)
    assert rewritten == content

    content = '# foo\\\n' \
              'SPAMDOPTIONS="--ssl-version tlsv1 -d --ssl"\n'  # still a comment
    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_option(content, ops)
    assert rewritten == content


def test_rewrite_spamd_option_last_assignment_takes_effect():
    ops = [spamassassinconfigupdate_spamd._drop_ssl_version, spamassassinconfigupdate_spamd._drop_daemonize_option]

    # Only the last assignment to a variable takes effect, so that's the only
    # assignment that we need to take care of
    content = 'SPAMDOPTIONS="-c -d"\n' \
              'SPAMDOPTIONS="-c -d -H"\n'
    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_option(content, ops)
    assert rewritten == '%s\nSPAMDOPTIONS="-c  -H"\n' % content.split('\n')[0]

    content = 'SPAMDOPTIONS="--ssl-version tlsv1 --ssl"\n' \
              'SPAMDOPTIONS="-c --ssl --ssl-version sslv3 -H"\n'
    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_option(content, ops)
    assert rewritten == '%s\nSPAMDOPTIONS="-c --ssl  -H"\n' % content.split('\n')[0]


def test_rewrite_spamd_option_multiline_value():
    ops = [spamassassinconfigupdate_spamd._drop_ssl_version, spamassassinconfigupdate_spamd._drop_daemonize_option]
    content = 'SPAMDOPTIONS="--ssl \\\n--ssl-version sslv3"\n'
    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_option(content, ops)
    assert rewritten == 'SPAMDOPTIONS="--ssl "\n'


def test_rewrite_spamd_config():
    facts = SpamassassinFacts(spamd_ssl_version='tlsv1', service_overriden=False)
    content = '# Options passed to spamd\n' \
              'SPAMDOPTIONS="-c -d -m5 --ssl -H --ssl-version tlsv1"\n'

    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_config(facts, content)

    assert rewritten == '# Options passed to spamd\n' \
                        'SPAMDOPTIONS="-c  -m5 --ssl -H "\n'


def test_rewrite_spamd_config_service_overriden():
    # If the service is overriden, the service type (simple/forking) remains
    # the same after upgrade. So we must not remove the -d option.
    facts = SpamassassinFacts(spamd_ssl_version='sslv3', service_overriden=True)
    content = '# Options to spamd\n' \
              'SPAMDOPTIONS="-c -d -m5 --ssl -H --ssl-version sslv3"\n'

    rewritten = spamassassinconfigupdate_spamd._rewrite_spamd_config(facts, content)

    assert rewritten == '# Options to spamd\n' \
                        'SPAMDOPTIONS="-c -d -m5 --ssl -H "\n'


class MockBackup(object):
    def __init__(self, to_raise=None):
        self.to_raise = to_raise
        self.called = 0
        self.paths = []

    def __call__(self, path):
        self.called += 1
        self.paths.append(path)
        if self.to_raise:
            raise self.to_raise
        return '/path/to/backup'


class MockFileOperations(object):
    def __init__(self, read_error=None, write_error=None):
        self.files = {}
        self.files_read = {}
        self.files_written = {}
        self.read_called = 0
        self.write_called = 0
        self.read_error = read_error
        self.write_error = write_error

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        if self.read_error:
            raise self.read_error
        return self.files[path]

    def _increment_write_counters(self, path):
        self.write_called += 1
        self.files_written.setdefault(path, 0)
        self.files_written[path] += 1

    def write(self, path, content):
        self._increment_write_counters(path)
        if self.write_error:
            raise self.write_error
        self.files[path] = content


def test_migrate_spamd_config():
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    fileops = MockFileOperations()
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version tlsv1 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    assert backup_func.called == 1
    assert backup_func.paths[0] == SYSCONFIG_SPAMASSASSIN
    expected_content = ('# foo\n' +
                        SYSCONFIG_VARIABLE + '="-c --ssl -hx"\n' +
                        '# bar \n')
    assert fileops.files[SYSCONFIG_SPAMASSASSIN] == expected_content
    assert fileops.read_called == 1
    assert fileops.files_read[SYSCONFIG_SPAMASSASSIN] == 1
    assert fileops.write_called == 1
    assert fileops.files_written[SYSCONFIG_SPAMASSASSIN] == 1


def test_migrate_spamd_config_nothing_to_migrate():
    facts = SpamassassinFacts(service_overriden=True, spamd_ssl_version=None)
    fileops = MockFileOperations()
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    assert fileops.read_called == 0
    assert fileops.write_called == 0
    assert backup_func.called == 0


def test_migrate_spamd_config_no_write_if_backup_fails():
    # OSError (e.g. os.open)
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    fileops = MockFileOperations()
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version tlsv1 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup(to_raise=make_OSError(errno.EACCES))

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    assert backup_func.called == 1
    assert fileops.write_called == 0

    # IOError (e.g. file.read)
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    fileops = MockFileOperations()
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version tlsv1 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup(to_raise=make_IOError(errno.EACCES))

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    assert backup_func.called == 1
    assert fileops.write_called == 0


def test_migrate_spamd_config_read_failure():
    # OSError (e.g. os.open)
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    fileops = MockFileOperations(read_error=make_OSError(errno.EACCES))
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version tlsv1 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    # The main purpose of this test is to check that exceptions are handled
    # properly. The following assertions are supplementary.
    assert fileops.read_called == 1
    assert fileops.write_called == 0

    # IOError (e.g. builtin open)
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    fileops = MockFileOperations(read_error=make_IOError(errno.EACCES))
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version tlsv1 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    assert fileops.read_called == 1
    assert fileops.write_called == 0


def test_migrate_spamd_config_write_failure():
    # OSError (e.g. os.open)
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    fileops = MockFileOperations(write_error=make_OSError(errno.EACCES))
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version tlsv1 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    # The main purpose of this test is to check that exceptions are handled
    # properly. The following assertions are supplementary.
    assert fileops.read_called == 1
    assert fileops.write_called == 1

    # IOError (e.g. builtin open)
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    fileops = MockFileOperations(write_error=make_IOError(errno.EACCES))
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version tlsv1 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    assert fileops.read_called == 1
    assert fileops.write_called == 1


def test_migrate_spamd_config_no_writes_with_unmodified_config():
    # Test that no writes are performed if the sysconfig file is in its
    # default state and thus gets replaced during the RPM upgrade.
    facts = SpamassassinFacts(service_overriden=False, spamd_ssl_version='tlsv1')
    # Content of the sysconfig file from RHEL-8
    content = ('# Options to spamd\n'
               'SPAMDOPTIONS="-c -m5 -H --razor-home-dir=\'/var/lib/razor/\' --razor-log-file=\'sys-syslog\'"\n')
    fileops = MockFileOperations()
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate_spamd.migrate_spamd_config(facts, fileops, backup_func)

    assert backup_func.called == 0
    assert fileops.read_called == 1
    assert fileops.write_called == 0


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/tests/test_library_spamassassinconfigupdate.py

from leapp.libraries.actor import spamassassinconfigupdate
from leapp.libraries.common.spamassassinutils import SPAMC_CONFIG_FILE, SYSCONFIG_SPAMASSASSIN, SYSCONFIG_VARIABLE
from leapp.models.spamassassinfacts import SpamassassinFacts


class MockBackup(object):
    def __init__(self, to_raise=None):
        self.to_raise = to_raise
        self.called = 0
        self.paths = []

    def __call__(self, path):
        self.called += 1
        self.paths.append(path)
        if self.to_raise:
            raise self.to_raise
        return '/path/to/backup'


class MockFileOperations(object):
    def __init__(self, read_error=None, write_error=None):
        self.files = {}
        self.files_read = {}
        self.files_written = {}
        self.read_called = 0
        self.write_called = 0
        self.read_error = read_error
        self.write_error = write_error

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        if self.read_error:
            raise self.read_error
        return self.files[path]

    def _increment_write_counters(self, path):
        self.write_called += 1
        self.files_written.setdefault(path, 0)
        self.files_written[path] += 1

    def write(self, path, content):
        self._increment_write_counters(path)
        if self.write_error:
            raise self.write_error
        self.files[path] = content


def test_migrate_configs():
    facts = SpamassassinFacts(spamc_ssl_argument='sslv3',
                              spamd_ssl_version='sslv3',
                              service_overriden=False)
    fileops = MockFileOperations()
    fileops.files[SPAMC_CONFIG_FILE] = '--ssl sslv3\n# foo\n-B\n'
    content = ('# foo\n' +
               SYSCONFIG_VARIABLE + '="-c --ssl-version sslv3 -hdx"\n' +
               '# bar \n')
    fileops.files[SYSCONFIG_SPAMASSASSIN] = content
    backup_func = MockBackup()

    spamassassinconfigupdate.migrate_configs(facts, fileops, backup_func)

    assert backup_func.called == 2
    assert SPAMC_CONFIG_FILE in backup_func.paths
    assert SYSCONFIG_SPAMASSASSIN in backup_func.paths
    assert fileops.files[SPAMC_CONFIG_FILE] == '--ssl\n# foo\n-B\n'
    expected_content = ('# foo\n' +
                        SYSCONFIG_VARIABLE + '="-c --ssl -hx"\n' +
                        '# bar \n')
    assert fileops.files[SYSCONFIG_SPAMASSASSIN] == expected_content


# repos/system_upgrade/el7toel8/actors/sssdfacts/libraries/sssdfacts.py

from leapp.models import SSSDConfig, SSSDDomainConfig


class SSSDFactsLibrary(object):
    """
    Helper library from SSSDFacts actor to allow unit testing.
    """
    def __init__(self, config):
        self.config = config

    def process(self):
        """
        Check SSSD configuration for changes in RHEL8 and return them in SSSDConfig
        model.

        These changes are:
        - id_provider=local is no longer supported and will be ignored
        - ldap_groups_use_matching_rule_in_chain was removed and will be ignored
        - ldap_initgroups_use_matching_rule_in_chain was removed and will be ignored
        - ldap_sudo_include_regexp changed default from true to false
        """
        facts = SSSDConfig(domains=[])

        for section in self.config.sections():
            # We are interested only in domains.
            if not section.startswith('domain/'):
                continue

            domain = SSSDDomainConfig(
                name=section[len('domain/'):],
                options=[]
            )

            steps = {
                'local_provider': self.checkLocalProvider(domain.name),
                'groups_chain': self.checkRemovedOption(
                    domain.name, 'ldap_groups_use_matching_rule_in_chain'
                ),
                'initgroups_chain': self.checkRemovedOption(
                    domain.name, 'ldap_initgroups_use_matching_rule_in_chain'
                ),
                'sudo_regexp': self.checkSudoRegexp(domain.name)
            }

            for key, value in steps.items():
                if value:
                    domain.options.append(key)

            facts.domains.append(domain)

        return facts

    def checkLocalProvider(self, domain):
        """
        Check if any id_provider=local is configured.
        """
        provider = self.get_provider(domain, 'id_provider')
        if provider != 'local':
            return False

        return True

    def checkRemovedOption(self, domain, option):
        """
        Check if specific option that has been removed is present.
        """
        section = self.get_domain_section(domain)
        if not self.config.has_option(section, option):
            return False

        return True

    def checkSudoRegexp(self, domain):
        """
        Check if ldap_sudo_include_regexp is not set explicitly.
        """
        section = self.get_domain_section(domain)
        provider = self.get_provider(domain, 'sudo_provider', ['id_provider'])

        if provider not in ['ad', 'ldap']:
            return False

        if self.config.has_option(section, 'ldap_sudo_include_regexp'):
            return False

        return True

    def get_domain_section(self, domain):
        """
        Convert domain name to ini format section.
        """
        return 'domain/' + domain

    def get_provider(self, domain, provider, fallbacks=None):
        """
        Return configured provider.
        """
        providers = [provider] + (fallbacks if fallbacks else [])

        section = self.get_domain_section(domain)
        for option in providers:
            if self.config.has_option(section, option):
                return self.config.get(section, option)

        return None


# repos/system_upgrade/el7toel8/actors/sssdfacts/tests/unit_test_sssdfacts.py

import textwrap

from six import StringIO

from leapp.libraries.actor.sssdfacts import SSSDFactsLibrary
from leapp.libraries.common import utils
from leapp.models import SSSDConfig, SSSDDomainConfig


def get_config(content):
    parser = utils.parse_config(StringIO(textwrap.dedent(content)))
    return parser


def test_empty_config():
    config = utils.parse_config()
    facts = SSSDFactsLibrary(config).process()

    assert not facts.domains


def test_local_domain():
    config = get_config("""
    [domain/local]
    id_provider = local
    """)

    facts = SSSDFactsLibrary(config).process()

    assert len(facts.domains) == 1
    assert facts.domains[0].name == "local"
    assert len(facts.domains[0].options) == 1
    assert "local_provider" in facts.domains[0].options


def test_groups_chain():
    config = get_config("""
    [domain/ldap]
    id_provider = ldap
    ldap_groups_use_matching_rule_in_chain = True

    # Set this option to avoid reporting its change
    ldap_sudo_include_regexp = True
    """)

    facts = SSSDFactsLibrary(config).process()

    assert len(facts.domains) == 1
    assert facts.domains[0].name == "ldap"
    assert len(facts.domains[0].options) == 1
    assert "groups_chain" in facts.domains[0].options


def test_initgroups_chain():
    config = get_config("""
    [domain/ldap]
    id_provider = ldap
    ldap_initgroups_use_matching_rule_in_chain = True

    # Set this option to avoid reporting its change
    ldap_sudo_include_regexp = True
    """)

    facts = SSSDFactsLibrary(config).process()

    assert len(facts.domains) == 1
    assert facts.domains[0].name == "ldap"
    assert len(facts.domains[0].options) == 1
    assert "initgroups_chain" in facts.domains[0].options


def test_sudo_regexp__ldap():
    config = get_config("""
    [domain/ldap]
    id_provider = ldap
    """)

    facts = SSSDFactsLibrary(config).process()

    assert len(facts.domains) == 1
    assert facts.domains[0].name == "ldap"
    assert len(facts.domains[0].options) == 1
    assert "sudo_regexp" in facts.domains[0].options


def test_sudo_regexp__ad():
    config = get_config("""
    [domain/ad]
    id_provider = ad
    """)

    facts = SSSDFactsLibrary(config).process()

    assert len(facts.domains) == 1
    assert facts.domains[0].name == "ad"
    assert len(facts.domains[0].options) == 1
    assert "sudo_regexp" in facts.domains[0].options


def test_sudo_regexp__ipa():
    config = get_config("""
    [domain/ipa]
    id_provider = ipa
    """)

    facts = SSSDFactsLibrary(config).process()

    assert len(facts.domains) == 1
    assert facts.domains[0].name == "ipa"
    assert not facts.domains[0].options


def test_complex():
    config = get_config("""
    [sssd]
    user = root
    services = nss, pam, sudo

    [domain/local]
    id_provider = local

    [domain/ldap]
    id_provider = ldap
    ldap_groups_use_matching_rule_in_chain = True

    [domain/ipa]
    id_provider = ipa
    """)

    facts = SSSDFactsLibrary(config).process()

    assert len(facts.domains) == 3

    assert facts.domains[0].name == "local"
    assert len(facts.domains[0].options) == 1
    assert "local_provider" in facts.domains[0].options

    assert facts.domains[1].name == "ldap"
    assert len(facts.domains[1].options) == 2
    assert "groups_chain" in facts.domains[1].options
    assert "sudo_regexp" in facts.domains[1].options

    assert facts.domains[2].name == "ipa"
    assert not facts.domains[2].options


def test_get_domain_section():
    config = utils.parse_config()
    library = SSSDFactsLibrary(config)

    assert library.get_domain_section("ldap") == "domain/ldap"


def test_get_provider__none():
    config = get_config("""
    [domain/local]
    """)
    library = SSSDFactsLibrary(config)

    assert library.get_provider("local", "id_provider") is None


def test_get_provider__without_fallback():
    config = get_config("""
    [domain/local]
    id_provider = local
    """)
    library = SSSDFactsLibrary(config)

    assert library.get_provider("local", "id_provider") == "local"


def test_get_provider__with_fallback():
    config = get_config("""
    [domain/ldap]
    id_provider = ldap
    """)
    library = SSSDFactsLibrary(config)

    assert library.get_provider("ldap", "sudo_provider", ["id_provider"]) == "ldap"


# repos/system_upgrade/el7toel8/actors/tcpwrapperscheck/libraries/tcpwrapperscheck.py

from leapp.libraries.common.tcpwrappersutils import config_applies_to_daemon


def config_affects_daemons(tcp_wrappers_facts, packages_list, daemons):
    """
    Check whether some of the daemons is installed and affected by existing
    configuration of tcp_wrappers based on the.

    :param tcp_wrappers_facts: Facts provided by the TcpWrappersFacts
    :param packages_list: List of packages provided by InstalledRedHatSignedRPM
    :param deamons: List of packages and keywords affecting daemons in this format:
                    [{"package-name", ["daemon1", "daemon2", ...], ...}]
    """
    found_packages = set()

    for (package, keywords) in daemons:
        # We do not care for particular deamon if the providing package is not installed
        if package not in packages_list:
            continue

        # Every package can have several deamons or deamons reacting to several keywords
        for daemon in keywords:
            # Is this daemon/keyword affected by the current configuration?
            if not config_applies_to_daemon(tcp_wrappers_facts, daemon):
                continue

            # We do not report particular daemons, but just the high-level list of packages
            found_packages.add(package)

    return found_packages


# repos/system_upgrade/el7toel8/actors/tcpwrapperscheck/tests/test_tcp_wrappers_tcpwrapperscheck.py

from leapp.libraries.actor.tcpwrapperscheck import config_affects_daemons
from leapp.models import TcpWrappersFacts, DaemonList


def test_empty_packages():
    """ Test with empty package list """
    w = TcpWrappersFacts(daemon_lists=[DaemonList(value=["ALL"])])
    p = []
    d = [("openssh", ["sshd"])]
    packages = config_affects_daemons(w, p, d)

    assert not packages


def test_empty_tcp_wrappers():
    """ Test with empty tcp_wrappers facts """
    w = TcpWrappersFacts(daemon_lists=[])
    p = ["openssh", "systemd", "pam"]
    d = [("openssh", ["sshd"])]
    packages = config_affects_daemons(w, p, d)

    assert not packages


def test_matching_package():
    """ Test with matching package, but not daemon """
    w = TcpWrappersFacts(daemon_lists=[DaemonList(value=["imap"])])
    p = ["openssh", "systemd", "pam"]
    d = [("openssh", ["sshd"])]
    packages = config_affects_daemons(w, p, d)

    assert not packages


def test_matching_daemon():
    """ Test with matching package with daemon """
    w = TcpWrappersFacts(daemon_lists=[DaemonList(value=["sshd"])])
    p = ["openssh", "systemd", "pam"]
    d = [("openssh", ["sshd"])]
    packages = config_affects_daemons(w, p, d)

    assert len(packages) == 1
    assert "openssh" in packages


def test_matching_all():
    """ Test with matching package with daemon """
    w = TcpWrappersFacts(daemon_lists=[DaemonList(value=["ALL"])])
    p = ["openssh", "systemd", "pam", "audit"]
    d = [("openssh", ["sshd"]), ("audit", ["auditd"])]
    packages = config_affects_daemons(w, p, d)

    assert len(packages) == 2
    assert "openssh" in packages
    assert "audit" in packages


# repos/system_upgrade/el7toel8/actors/tcpwrappersconfigread/libraries/tcpwrappersconfigread.py

import errno
import re

from leapp.libraries.stdlib import api
from leapp.models import DaemonList, TcpWrappersFacts

CONFIG_FILES = ['/etc/hosts.allow', '/etc/hosts.deny']


def _read_file(path):
    with open(path, 'r') as f:
        return f.read()


def _get_lines(content):
    content = re.sub(r'\\\n', '', content)
    return content.split('\n')


def _is_comment(line):
    return len(line) == 0 or line.isspace() or line.startswith('#')


def _get_daemon_list_in_line(line):
    daemon_list = line.split(':', 1)[0]
    daemon_list = re.split(',| |\t', daemon_list)
    daemon_list = [word for word in daemon_list if len(word) > 0]
    return DaemonList(value=daemon_list)


def _get_daemon_lists_in_file(path, read_func=_read_file):
    ret = []
    try:
        content = read_func(path)
    except IOError as e:
        if e.errno != errno.ENOENT:
            api.current_logger().warning('Failed to read %s: %s' % (path, e))
        return ret
    lines = [line for line in _get_lines(content) if not _is_comment(line)]
    for line in lines:
        ret.append(_get_daemon_list_in_line(line))
    return ret


def _get_daemon_lists(read_func=_read_file):
    daemon_lists = []
    for path in CONFIG_FILES:
        daemon_lists.extend(_get_daemon_lists_in_file(path, read_func=read_func))
    return daemon_lists


def get_tcp_wrappers_facts(read_func=_read_file):
    daemon_lists = _get_daemon_lists(read_func=read_func)
    return TcpWrappersFacts(daemon_lists=daemon_lists)


# repos/system_upgrade/el7toel8/actors/tcpwrappersconfigread/tests/test_library_tcpwrappersconfigread.py

import errno

from leapp.libraries.actor import tcpwrappersconfigread
from leapp.libraries.common.testutils import make_IOError


class MockFileReader(object):
    def __init__(self):
        self.files = {}
        self.files_read = {}
        self.read_called = 0

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        try:
            return self.files[path]
        except KeyError:
            raise make_IOError(errno.ENOENT)


def test_get_daemon_list_in_line_simple():
    line = 'vsftpd : 192.168.2.*'
    daemon_list = tcpwrappersconfigread._get_daemon_list_in_line(line)
    assert daemon_list.value == ['vsftpd']


def test_get_daemon_list_in_line_multiple():
    line = 'vsftpd, sendmail : 192.168.2.*'
    daemon_list = tcpwrappersconfigread._get_daemon_list_in_line(line)
    assert daemon_list.value == ['vsftpd', 'sendmail']

    line = 'ALL EXCEPT sendmail : 192.168.2.*'
    daemon_list = tcpwrappersconfigread._get_daemon_list_in_line(line)
    assert daemon_list.value == ['ALL', 'EXCEPT', 'sendmail']

    # different separators
    line = 'vsftpd,sendmail : 192.168.2.*'
    daemon_list = tcpwrappersconfigread._get_daemon_list_in_line(line)
    assert daemon_list.value == ['vsftpd', 'sendmail']

    line = 'vsftpd\tsendmail : 192.168.2.*'
    daemon_list = tcpwrappersconfigread._get_daemon_list_in_line(line)
    assert daemon_list.value == ['vsftpd', 'sendmail']

    line = 'vsftpd, \t sendmail : 192.168.2.*'
    daemon_list = tcpwrappersconfigread._get_daemon_list_in_line(line)
    assert daemon_list.value == ['vsftpd', 'sendmail']


def test_get_daemon_list_in_line_malformed():
    line = 'foo'
    daemon_list = tcpwrappersconfigread._get_daemon_list_in_line(line)
    # tcp_wrappers actually ignores lines like this, but there's no harm in being
    # over-sensitive here.
    assert daemon_list.value == ['foo']


def test_get_lines_empty():
    content = ''
    lines = tcpwrappersconfigread._get_lines(content)
    assert lines == ['']


def test_get_lines_simple():
    content = 'vsftpd : 192.168.2.*\n' \
              'ALL : 192.168.1.*\n'
    lines = tcpwrappersconfigread._get_lines(content)
    assert lines == content.split('\n')


def test_get_lines_continued_line():
    content = 'vsftpd : 192.168\\\n.2.*'
    lines = tcpwrappersconfigread._get_lines(content)
    expected = ['vsftpd : 192.168.2.*']
    assert lines == expected


def test_get_lines_backslash_followed_by_whitespace():
    content = 'foo \\ \nthis is not a continuation line'
    lines = tcpwrappersconfigread._get_lines(content)
    expected = ['foo \\ ', 'this is not a continuation line']
    assert lines == expected


def test_get_lines_continued_comment():
    content = '# foo \\\n' \
              'this is still a comment'
    lines = tcpwrappersconfigread._get_lines(content)
    expected = ['# foo this is still a comment']
    assert lines == expected


def test_is_comment():
    assert tcpwrappersconfigread._is_comment('') is True
    assert tcpwrappersconfigread._is_comment('  ') is True
    assert tcpwrappersconfigread._is_comment('# foo') is True
    assert tcpwrappersconfigread._is_comment('#') is True
    assert tcpwrappersconfigread._is_comment(' # foo') is False
    assert tcpwrappersconfigread._is_comment('foo') is False
    assert tcpwrappersconfigread._is_comment(' foo') is False


def test_get_daemon_lists_in_file():
    path = '/etc/hosts.allow'
    reader = MockFileReader()
    reader.files[path] = 'vsftpd : 192.168.2.*\n' \
                         'ALL : 192.168.1.*\n'

    daemon_lists = tcpwrappersconfigread._get_daemon_lists_in_file(path, read_func=reader.read)

    num_lines = 2
    assert len(daemon_lists) == num_lines
    assert daemon_lists[0].value == ['vsftpd']
    assert daemon_lists[1].value == ['ALL']


def test_get_daemon_lists_in_file_nonexistent():
    reader = MockFileReader()
    daemon_lists = tcpwrappersconfigread._get_daemon_lists_in_file('/etc/hosts.allow', read_func=reader.read)
    assert not daemon_lists


def test_get_daemon_lists():
    reader = MockFileReader()
    reader.files['/etc/hosts.allow'] = 'vsftpd : 192.168.2.*\n' \
                                       'ALL : 192.168.1.*\n'
    reader.files['/etc/hosts.deny'] = 'sendmail : 192.168.2.*\n'

    daemon_lists = tcpwrappersconfigread._get_daemon_lists(read_func=reader.read)

    num_lines = 3
    assert len(daemon_lists) == num_lines
    assert daemon_lists[0].value == ['vsftpd']
    assert daemon_lists[1].value == ['ALL']
    assert daemon_lists[2].value == ['sendmail']


def test_get_daemon_lists_nonexistent_config():
    reader = MockFileReader()
    daemon_lists = tcpwrappersconfigread._get_daemon_lists(read_func=reader.read)
    assert not daemon_lists


def test_get_tcp_wrappers_facts():
    reader = MockFileReader()
    reader.files['/etc/hosts.allow'] = 'vsftpd : 192.168.2.*\n' \
                                       'ALL : 192.168.1.*\n'
    reader.files['/etc/hosts.deny'] = 'sendmail : 192.168.2.*\n'

    facts = tcpwrappersconfigread.get_tcp_wrappers_facts(read_func=reader.read)

    num_lines = 3
    assert len(facts.daemon_lists) == num_lines
    assert facts.daemon_lists[0].value == ['vsftpd']
    assert facts.daemon_lists[1].value == ['ALL']
    assert facts.daemon_lists[2].value == ['sendmail']


def test_get_tcp_wrappers_facts_nonexistent_config():
    reader = MockFileReader()
    facts = tcpwrappersconfigread.get_tcp_wrappers_facts(read_func=reader.read)
    assert not facts.daemon_lists


# repos/system_upgrade/el7toel8/actors/updateetcsysconfigkernel/libraries/updateetcsysconfigkernel.py

from leapp.libraries.stdlib import run


def update_kernel_config(path):
    """ Update DEFAULTKERNEL entry at provided config file """
    run(['/bin/sed',
         '-i',
         's/^DEFAULTKERNEL=kernel$/DEFAULTKERNEL=kernel-core/g',
         path])


# repos/system_upgrade/el7toel8/actors/updateetcsysconfigkernel/tests/unit_test_updateetcsysconfigkernel.py

import os
import tempfile

import pytest

from leapp.libraries.actor import updateetcsysconfigkernel


# TODO [Artem] could be solved
@pytest.mark.skip(reason='Failing on CI complaining about missing leapp.db fiel')
def test_update_kernel_config(monkeypatch):
    temp = tempfile.NamedTemporaryFile(delete=False)
    with open('tests/files/original') as f:
        data = f.readlines()
        temp.writelines(data)
    temp.close()

    updateetcsysconfigkernel.update_kernel_config(temp.name)

    with open(temp.name) as f:
        result = f.readlines()

    with open('tests/files/expected') as f:
        expected = f.readlines()

    assert result == expected

    os.unlink(temp.name)
    assert not os.path.exists(temp.name)


# repos/system_upgrade/el7toel8/actors/vimmigrate/libraries/vimmigrate.py

from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM


def _append_string(path, content):
    """
    Appends string into file

    :param str path: path to file
    :param str content: content to add
    """
    with open(path, 'a') as f:
        f.write(content)


# rpm : the default config file
vim_configs = {
    'vim-minimal': '/etc/virc',
    'vim-enhanced': '/etc/vimrc'
}


# list of macros that should be set
new_macros = [
    'let skip_defaults_vim=1',
    'set t_BE='
]


def update_config(path, append_function=_append_string):
    """
    Insert expected content into the file on the path

    :param str path: string representing the full path of the config file
    :param func append_function: appends string into file
    """
    fmt_input = "\n{comment_line}\n{content}\n".format(comment_line='" content added by Leapp',
                                                       content='\n'.join(new_macros))

    try:
        append_function(path, fmt_input)
    except IOError:
        raise IOError('Error during writing to file: {}.'.format(path))


def _check_package(pkg):
    """
    Checks if a package is installed and signed

    :param str pkg: name of package
    """
    return has_package(InstalledRedHatSignedRPM, pkg)


def update_vim(debug_log=api.current_logger().debug,
               error_log=api.current_logger().error,
               is_installed=_check_package,
               append_function=_append_string):
    """
    Do update of configuration files

    :param func debug_log: function for debug logging
    :param func error_log: function for error logging
    :param func is_installed: checks if a package is installed
    :param func append_function: appends string into file
    """
    error_list = []

    for pkg, config_file in vim_configs.items():
        if not is_installed(pkg):
            continue

        debug_log('Updating Vim configuration file {}.'.format(config_file))

        try:
            update_config(config_file, append_function)
        except (OSError, IOError) as error:
            error_list.append((config_file, error))
    if error_list:
        error_log('The files below have not been modified (error message included):' +
                  ''.join(['\n    - {}: {}'.format(err[0], err[1]) for err in error_list]))


# repos/system_upgrade/el7toel8/actors/vimmigrate/tests/test_update_config_vimmigrate.py

import pytest

from leapp.libraries.actor.vimmigrate import new_macros, update_config


class MockFile(object):
    def __init__(self, path, content=None):
        self.path = path
        self.content = content
        self.error = False

    def append(self, path, content):
        if path != self.path:
            self.error = True
        if not self.error:
            self.content += content
            return self.content
        raise IOError('Error during writing to file: {}.'.format(path))


def test_update_config_file_errors(path='foo'):
    f = MockFile(path, content='')

    with pytest.raises(IOError):
        update_config('bar', f.append)

    assert f.content == ''


@pytest.mark.parametrize('content', ('', 'bleblaba'))
def test_update_config_append_into_file(content):
    path = 'bar'

    fmt_input = "\n{comment_line}\n{content}\n".format(comment_line='" content added by Leapp',
                                                       content='\n'.join(new_macros))

    f = MockFile(path, content)
    res = update_config(path, f.append)

    assert res is None
    assert f.content == content + fmt_input


# repos/system_upgrade/el7toel8/actors/vimmigrate/tests/test_update_vim_vimmigrate.py

import pytest

from leapp.libraries.actor.vimmigrate import update_vim, vim_configs


packages = [
    {
        'vim-minimal': '/etc/virc',
        'vim-enhanced': '/etc/vimrc'
    },
    {
        'vim-minimal': '/etc/virc',
        'vim-enhanced': ''
    },
    {
        'vim-minimal': '',
        'vim-enhanced': '/etc/vimrc'
    },
    {
        'vim-minimal': '',
        'vim-enhanced': ''
    },
    {
        'vim-minimal': '/etc/virc',
        'ble': ''
    },
    {
        'vim-minimal': '',
        'ble': ''
    },
    {
        'vim-enhanced': '/etc/vimrc',
        'moo': ''
    },
    {
        'vim-enhanced': '',
        'moo': ''
    },
    {
        'you': '',
        'hele': ''
    }
]


class MockLogger(object):
    def __init__(self):
        self.debugmsg = ''
        self.errmsg = ''

    def debug(self, message):
        self.debugmsg += message

    def error(self, message):
        self.errmsg += message


class MockPackage(object):
    def __init__(self, name, config):
        self.name = name
        self.config = config


class MockPackageSet(object):
    def __init__(self):
        self.installed_packages = None

    def add_packages(self, pkgs):
        if self.installed_packages is None:
            self.installed_packages = []

        for rpm, config in pkgs.items():
            self.installed_packages.append(MockPackage(rpm, config))

    def is_installed(self, pkg):
        for rpm in self.installed_packages:
            if pkg == rpm.name:
                return True
        return False

    def append_content(self, path, content):
        found = False

        for rpm in self.installed_packages:
            if path == rpm.config:
                found = True
        if not found:
            raise IOError('Error during writing to file: {}.'.format(path))


class ExpectedOutput(object):
    def __init__(self):
        self.debugmsg = ''
        self.errmsg = ''

    def create(self, rpms):
        error_list = []

        for pkg, config in rpms.items():
            if pkg in vim_configs.keys():
                self.debugmsg += 'Updating Vim configuration file {}.'.format(vim_configs[pkg])
                if config == '':
                    error_list.append((vim_configs[pkg], 'Error during writing to file: {}.'.format(vim_configs[pkg])))

        if error_list:
            self.errmsg = ('The files below have not been modified '
                           '(error message included):' +
                           ''.join(['\n    - {}: {}'.format(err[0], err[1])
                                   for err in error_list]))


@pytest.mark.parametrize('rpms', packages)
def test_update_vim(rpms):
    logger = MockLogger()
    installed_packages = MockPackageSet()

    installed_packages.add_packages(rpms)

    expected = ExpectedOutput()
    expected.create(rpms)

    update_vim(logger.debug,
               logger.error,
               installed_packages.is_installed,
               installed_packages.append_content)

    assert expected.debugmsg == logger.debugmsg
    assert expected.errmsg == logger.errmsg


# repos/system_upgrade/el7toel8/actors/vsftpdconfigcheck/libraries/vsftpdconfigcheck.py

from leapp.reporting import create_report
from leapp import reporting
from leapp.libraries.common.tcpwrappersutils import config_applies_to_daemon


def check_config_supported(tcpwrap_facts, vsftpd_facts):
    bad_configs = [config.path for config in vsftpd_facts.configs if config.tcp_wrappers]
    if bad_configs and config_applies_to_daemon(tcpwrap_facts, 'vsftpd'):
        list_separator_fmt = '\n    - '
        create_report([
            reporting.Title('Unsupported vsftpd configuration'),
            reporting.Summary(
                    'tcp_wrappers support has been removed in RHEL-8. '
                    'Some configuration files set the tcp_wrappers option to true and '
                    'there is some vsftpd-related configuration in /etc/hosts.deny '
                    'or /etc/hosts.allow. Please migrate it manually. '
                    'The list of problematic configuration files:{}{}'.
                    format(
                        list_separator_fmt,
                        list_separator_fmt.join(bad_configs)
                    )
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.SERVICES, reporting.Tags.NETWORK]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.ExternalLink(
                title='Replacing TCP Wrappers in RHEL 8',
                url='https://access.redhat.com/solutions/3906701'
            ),
            reporting.RelatedResource('package', 'tcp_wrappers'),
            reporting.RelatedResource('package', 'vsftpd'),
        ] + [reporting.RelatedResource('file', str(bc)) for bc in bad_configs])


# repos/system_upgrade/el7toel8/actors/vsftpdconfigcheck/tests/test_library_vsftpdconfigcheck.py

from leapp.models import DaemonList, TcpWrappersFacts, VsftpdConfig, VsftpdFacts
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context


def test_actor_with_unsupported_tcpwrap_and_vsftpd_config(current_actor_context):
    config1 = VsftpdConfig(path='/etc/vsftpd/foo.conf', tcp_wrappers=False)
    config2 = VsftpdConfig(path='/etc/vsftpd/bar.conf', tcp_wrappers=True)
    vsftpd_facts = VsftpdFacts(configs=[config1, config2])
    daemon_list = DaemonList(value=['vsftpd'])
    tcpwrap_facts = TcpWrappersFacts(daemon_lists=[daemon_list])

    current_actor_context.feed(vsftpd_facts)
    current_actor_context.feed(tcpwrap_facts)
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report

    assert 'inhibitor' in report_fields['flags']
    assert 'foo.conf' not in report_fields['summary']
    assert 'bar.conf' in report_fields['summary']


def test_actor_with_unsupported_tcpwrap_multiple_unsupported_vsftpd_configs(current_actor_context):
    config1 = VsftpdConfig(path='/etc/vsftpd/foo.conf', tcp_wrappers=True)
    config2 = VsftpdConfig(path='/etc/vsftpd/bar.conf', tcp_wrappers=False)
    config3 = VsftpdConfig(path='/etc/vsftpd/goo.conf', tcp_wrappers=True)
    vsftpd_facts = VsftpdFacts(configs=[config1, config2, config3])
    daemon_list = DaemonList(value=['vsftpd'])
    tcpwrap_facts = TcpWrappersFacts(daemon_lists=[daemon_list])

    current_actor_context.feed(vsftpd_facts)
    current_actor_context.feed(tcpwrap_facts)
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report

    assert 'inhibitor' in report_fields['flags']
    assert 'foo.conf' in report_fields['summary']
    assert 'bar.conf' not in report_fields['summary']
    assert 'goo.conf' in report_fields['summary']


def test_actor_with_unsupported_tcpwrap_config(current_actor_context):
    config1 = VsftpdConfig(path='/etc/vsftpd/foo.conf', tcp_wrappers=False)
    config2 = VsftpdConfig(path='/etc/vsftpd/bar.conf', tcp_wrappers=None)
    vsftpd_facts = VsftpdFacts(configs=[config1, config2])
    daemon_list = DaemonList(value=['vsftpd'])
    tcpwrap_facts = TcpWrappersFacts(daemon_lists=[daemon_list])

    current_actor_context.feed(vsftpd_facts)
    current_actor_context.feed(tcpwrap_facts)
    current_actor_context.run()

    assert not current_actor_context.consume(Report)


def test_actor_with_unsupported_vsftpd_config(current_actor_context):
    config1 = VsftpdConfig(path='/etc/vsftpd/foo.conf', tcp_wrappers=False)
    config2 = VsftpdConfig(path='/etc/vsftpd/bar.conf', tcp_wrappers=True)
    vsftpd_facts = VsftpdFacts(configs=[config1, config2])
    daemon_list = DaemonList(value=['all', 'except', 'vsftpd'])
    tcpwrap_facts = TcpWrappersFacts(daemon_lists=[daemon_list])

    current_actor_context.feed(vsftpd_facts)
    current_actor_context.feed(tcpwrap_facts)
    current_actor_context.run()

    assert not current_actor_context.consume(Report)


def test_actor_with_supported_tcpwrap_and_vsftpd_config(current_actor_context):
    config1 = VsftpdConfig(path='/etc/vsftpd/foo.conf', tcp_wrappers=False)
    config2 = VsftpdConfig(path='/etc/vsftpd/bar.conf', tcp_wrappers=False)
    vsftpd_facts = VsftpdFacts(configs=[config1, config2])
    daemon_list = DaemonList(value=['all', 'except', 'vsftpd'])
    tcpwrap_facts = TcpWrappersFacts(daemon_lists=[daemon_list])

    current_actor_context.feed(vsftpd_facts)
    current_actor_context.feed(tcpwrap_facts)
    current_actor_context.run()

    assert not current_actor_context.consume(Report)


# repos/system_upgrade/el7toel8/actors/vsftpdconfigread/libraries/config_parser.py

class ParsingError(Exception):
    pass


class VsftpdConfigOptionParser(object):
    def _get_string_options(self):
        return ["secure_chroot_dir", "ftp_username", "chown_username", "xferlog_file",
                "vsftpd_log_file", "message_file", "nopriv_user", "ftpd_banner",
                "banned_email_file", "chroot_list_file", "pam_service_name", "guest_username",
                "userlist_file", "anon_root", "local_root", "banner_file", "pasv_address",
                "listen_address", "user_config_dir", "listen_address6", "cmds_allowed",
                "hide_file", "deny_file", "user_sub_token", "email_password_file",
                "rsa_cert_file", "dsa_cert_file", "dh_param_file", "ecdh_param_file",
                "ssl_ciphers", "rsa_private_key_file", "dsa_private_key_file", "ca_certs_file",
                "cmds_denied"]

    def _get_boolean_options(self):
        return ["anonymous_enable", "local_enable", "pasv_enable", "port_enable",
                "chroot_local_user", "write_enable", "anon_upload_enable",
                "anon_mkdir_write_enable", "anon_other_write_enable", "chown_uploads",
                "connect_from_port_20", "xferlog_enable", "dirmessage_enable",
                "anon_world_readable_only", "async_abor_enable", "ascii_upload_enable",
                "ascii_download_enable", "one_process_model", "xferlog_std_format",
                "pasv_promiscuous", "deny_email_enable", "chroot_list_enable",
                "setproctitle_enable", "text_userdb_names", "ls_recurse_enable",
                "log_ftp_protocol", "guest_enable", "userlist_enable", "userlist_deny",
                "use_localtime", "check_shell", "hide_ids", "listen", "port_promiscuous",
                "passwd_chroot_enable", "no_anon_password", "tcp_wrappers", "use_sendfile",
                "force_dot_files", "listen_ipv6", "dual_log_enable", "syslog_enable",
                "background", "virtual_use_local_privs", "session_support", "download_enable",
                "dirlist_enable", "chmod_enable", "secure_email_list_enable",
                "run_as_launching_user", "no_log_lock", "ssl_enable", "allow_anon_ssl",
                "force_local_logins_ssl", "force_local_data_ssl", "ssl_sslv2", "ssl_sslv3",
                "ssl_tlsv1", "ssl_tlsv1_1", "ssl_tlsv1_2", "tilde_user_enable",
                "force_anon_logins_ssl", "force_anon_data_ssl", "mdtm_write",
                "lock_upload_files", "pasv_addr_resolve", "reverse_lookup_enable",
                "userlist_log", "debug_ssl", "require_cert", "validate_cert",
                "strict_ssl_read_eof", "strict_ssl_write_shutdown", "ssl_request_cert",
                "delete_failed_uploads", "implicit_ssl", "ptrace_sandbox", "require_ssl_reuse",
                "isolate", "isolate_network", "ftp_enable", "http_enable", "seccomp_sandbox",
                "allow_writeable_chroot", "better_stou", "log_die"]

    def _get_integer_options(self):
        return ["accept_timeout", "connect_timeout", "local_umask", "anon_umask",
                "ftp_data_port", "idle_session_timeout", "data_connection_timeout",
                "pasv_min_port", "pasv_max_port", "anon_max_rate", "local_max_rate",
                "listen_port", "max_clients", "file_open_mode", "max_per_ip", "trans_chunk_size",
                "delay_failed_login", "delay_successful_login", "max_login_fails",
                "chown_upload_mode", "bind_retries"]

    def _get_boolean(self, option, value):
        value = value.upper()
        if value in ['YES', 'TRUE', '1']:
            return True
        if value in ['NO', 'FALSE', '0']:
            return False
        raise ParsingError("Boolean option '%s' contains a non-boolean value '%s'"
                           % (option, value))

    def _get_integer(self, option, value):
        try:
            return int(value)
        except ValueError:
            raise ParsingError("Integer option '%s' contains a non-integer value '%s'"
                               % (option, value))

    def parse_value(self, option, value):
        if option in self._get_string_options():
            return value
        if option in self._get_boolean_options():
            return self._get_boolean(option, value)
        if option in self._get_integer_options():
            return self._get_integer(option, value)

        raise ParsingError("Unknown option: '%s'" % option)


class VsftpdConfigParser(object):
    def __init__(self, config_content):
        self._option_parser = VsftpdConfigOptionParser()
        self.parsed_config = self._parse_config(config_content)

    def _parse_config_line(self, line, conf_dict):
        if not line or line.startswith('#') or line.isspace():
            return
        try:
            option, value = line.split('=', 1)
        except ValueError:
            raise ParsingError("The line does not have the form 'option=value': %s" % line)
        option = option.strip()
        value = value.strip()
        value = self._option_parser.parse_value(option, value)
        conf_dict[option] = value

    def _parse_config(self, contents):
        res = {}
        try:
            for (ix, line) in enumerate(contents.split('\n')):
                self._parse_config_line(line, res)
            return res
        except ParsingError as e:
            raise ParsingError("Syntax error on line %d: %s" % (ix + 1, e))


# repos/system_upgrade/el7toel8/actors/vsftpdconfigread/libraries/vsftpdconfigread.py

import errno
import os

from leapp.libraries.actor import config_parser
from leapp.libraries.common import vsftpdutils as utils
from leapp.libraries.stdlib import api
from leapp.models import VsftpdConfig, VsftpdFacts


def _parse_config(path, content):
    try:
        parser = config_parser.VsftpdConfigParser(content)
        return parser.parsed_config
    except config_parser.ParsingError:
        api.current_logger().info('File %s does not look like vsftpd configuration, skipping.'
                                  % path)
        return None


def _get_parsed_configs(read_func=utils.read_file, listdir=os.listdir):
    res = []
    try:
        for fname in listdir(utils.VSFTPD_CONFIG_DIR):
            path = os.path.join(utils.VSFTPD_CONFIG_DIR, fname)
            if not path.endswith('.conf'):
                continue
            content = utils.get_config_contents(path, read_func=read_func)
            if content is None:
                continue
            parsed = _parse_config(path, content)
            if parsed is not None:
                res.append((path, parsed))
    except OSError as e:
        if e.errno != errno.ENOENT:
            api.current_logger().warning('Failed to read vsftpd configuration directory: %s'
                                         % e)
    return res


def get_vsftpd_facts(read_func=utils.read_file, listdir=os.listdir):
    config_hash = utils.get_default_config_hash(read_func=read_func)
    configs = _get_parsed_configs(read_func=read_func, listdir=listdir)
    res_configs = []
    for path, config in configs:
        res_configs.append(VsftpdConfig(path=path,
                                        strict_ssl_read_eof=config.get(utils.STRICT_SSL_READ_EOF),
                                        tcp_wrappers=config.get(utils.TCP_WRAPPERS)))
    return VsftpdFacts(default_config_hash=config_hash, configs=res_configs)


def is_processable(installed_rpm_facts):
    for pkg in installed_rpm_facts.items:
        if pkg.name == 'vsftpd':
            return True
    return False


# repos/system_upgrade/el7toel8/actors/vsftpdconfigread/tests/test_config_parser_vsftpdconfigread.py

import pytest

from leapp.libraries.actor.config_parser import ParsingError, VsftpdConfigOptionParser, \
    VsftpdConfigParser


def test_VsftpdConfigOptionParser_invalid_syntax():
    parser = VsftpdConfigOptionParser()

    with pytest.raises(ParsingError):
        parser.parse_value('unknown option', 'foo')
    with pytest.raises(ParsingError):
        parser.parse_value('anonymous_enable', 'non-boolean value')
    with pytest.raises(ParsingError):
        parser.parse_value('require_cert', 'non-boolean value')
    with pytest.raises(ParsingError):
        parser.parse_value('anon_mkdir_write_enable', '')
    with pytest.raises(ParsingError):
        parser.parse_value('accept_timeout', 'non-integer value')
    with pytest.raises(ParsingError):
        parser.parse_value('max_per_ip', 'non-integer value')
    with pytest.raises(ParsingError):
        parser.parse_value('listen_port', '')


def test_VsftpdConfigOptionParser_string_option():
    parser = VsftpdConfigOptionParser()

    assert parser.parse_value('secure_chroot_dir', 'foo') == 'foo'
    assert parser.parse_value('user_config_dir', '') == ''
    assert parser.parse_value('dsa_cert_file', 'value with spaces') == 'value with spaces'


def test_VsftpdConfigOptionParser_boolean_option():
    parser = VsftpdConfigOptionParser()

    assert parser.parse_value('background', 'TRUE') is True
    assert parser.parse_value('run_as_launching_user', 'true') is True
    assert parser.parse_value('no_log_lock', 'YES') is True
    assert parser.parse_value('force_local_data_ssl', 'yES') is True
    assert parser.parse_value('ssl_tlsv1_2', '1') is True

    assert parser.parse_value('background', 'FALSE') is False
    assert parser.parse_value('run_as_launching_user', 'false') is False
    assert parser.parse_value('no_log_lock', 'NO') is False
    assert parser.parse_value('force_local_data_ssl', 'No') is False
    assert parser.parse_value('ssl_tlsv1_2', '0') is False


def test_VsftpdConfigOptionParser_integer_option():
    parser = VsftpdConfigOptionParser()

    assert parser.parse_value('connect_timeout', '0') == 0
    assert parser.parse_value('idle_session_timeout', '1') == 1
    assert parser.parse_value('data_connection_timeout', '2') == 2
    assert parser.parse_value('pasv_max_port', '6234') == 6234


def test_VsftpdConfigParser_invalid_syntax():
    with pytest.raises(ParsingError):
        VsftpdConfigParser('unknown_option=foo')
    with pytest.raises(ParsingError):
        VsftpdConfigParser('anonymous_enable=non-boolean')
    with pytest.raises(ParsingError):
        VsftpdConfigParser(' # comment with whitespace before the # character')
    with pytest.raises(ParsingError):
        VsftpdConfigParser('anonymous_enable')


def test_VsftpdConfigParser_empty_config():
    parser = VsftpdConfigParser('')
    assert isinstance(parser.parsed_config, dict)
    assert not parser.parsed_config


def test_VsftpdConfigParser_only_comments():
    parser = VsftpdConfigParser('# foo\n\n#bar\n')
    assert isinstance(parser.parsed_config, dict)
    assert not parser.parsed_config

    parser = VsftpdConfigParser('#anonymous_enable=yes\n')
    assert isinstance(parser.parsed_config, dict)
    assert not parser.parsed_config


def test_VsftpdConfigParser_one_option():
    parser = VsftpdConfigParser('anonymous_enable=yes\n')
    assert len(parser.parsed_config) == 1
    assert parser.parsed_config['anonymous_enable'] is True


def test_VsftpdConfigParser_multiple_options():
    content = '# foo\n\nanonymous_enable=no\nbanned_email_file=/foo/bar\n# bar\nmax_login_fails=3\n'
    parser = VsftpdConfigParser(content)

    assert len(parser.parsed_config) == 3
    assert parser.parsed_config['anonymous_enable'] is False
    assert parser.parsed_config['banned_email_file'] == '/foo/bar'
    assert parser.parsed_config['max_login_fails'] == 3


# repos/system_upgrade/el7toel8/actors/vsftpdconfigread/tests/test_library_vsftpdconfigread.py

import errno
import os

from leapp.libraries.actor import vsftpdconfigread
from leapp.libraries.common.testutils import make_IOError, make_OSError
from leapp.models import InstalledRedHatSignedRPM, RPM


class MockFileOperations(object):
    def __init__(self):
        self.files = {}
        self.files_read = {}
        self.read_called = 0

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        try:
            return self.files[path]
        except KeyError:
            raise make_IOError(errno.ENOENT)


class MockListDir(object):
    def __init__(self, path=None, file_names=None, to_raise=None):
        self.path = None if path is None else os.path.normpath(path)
        self.file_names = file_names
        self.to_raise = to_raise
        self.error = False

    def listdir(self, path):
        if self.to_raise:
            raise self.to_raise
        if os.path.normpath(path) == self.path:
            return self.file_names

        self.error = True
        raise make_OSError(errno.ENOENT)


def test_parse_config():
    content = 'anonymous_enable=YES'
    path = 'my_file'

    parsed = vsftpdconfigread._parse_config(path, content)

    assert parsed['anonymous_enable'] is True


def test_parsing_bad_config_gives_None():
    content = 'foo'
    path = 'my_file'

    parsed = vsftpdconfigread._parse_config(path, content)

    assert parsed is None


def test_get_parsed_configs():
    directory = '/etc/vsftpd'
    file_names = ['vsftpd.conf', 'foo.conf']
    listdir = MockListDir(directory, file_names)
    fileops = MockFileOperations()
    fileops.files[os.path.join(directory, file_names[0])] = 'anonymous_enable=YES\n' \
                                                            'ca_certs_file=/foo/bar\n'
    fileops.files[os.path.join(directory, file_names[1])] = 'anonymous_enable=NO\n'

    parsed_configs = list(vsftpdconfigread._get_parsed_configs(read_func=fileops.read,
                                                               listdir=listdir.listdir))

    assert not listdir.error
    assert len(fileops.files_read) == 2
    assert os.path.join(directory, file_names[0]) in fileops.files_read
    assert os.path.join(directory, file_names[1]) in fileops.files_read
    assert len(parsed_configs) == 2
    if parsed_configs[0][0] != os.path.join(directory, file_names[0]):
        parsed_configs.reverse()
    assert (os.path.join(directory, file_names[0]), {'anonymous_enable': True,
                                                     'ca_certs_file': '/foo/bar'}) in parsed_configs
    assert (os.path.join(directory, file_names[1]), {'anonymous_enable': False}) in parsed_configs


def test_get_parsed_configs_empty_dir():
    directory = '/etc/vsftpd'
    listdir = MockListDir(directory, [])
    fileops = MockFileOperations()

    parsed_configs = vsftpdconfigread._get_parsed_configs(read_func=fileops.read,
                                                          listdir=listdir.listdir)

    assert not listdir.error
    assert fileops.read_called == 0
    assert not parsed_configs


def test_get_parsed_configs_nonexistent_dir():
    listdir = MockListDir(to_raise=make_OSError(errno.ENOENT))
    fileops = MockFileOperations()

    parsed_configs = vsftpdconfigread._get_parsed_configs(read_func=fileops.read,
                                                          listdir=listdir.listdir)

    assert fileops.read_called == 0
    assert not parsed_configs


def test_get_parsed_configs_inaccessible_dir():
    listdir = MockListDir(to_raise=make_OSError(errno.EACCES))
    fileops = MockFileOperations()

    parsed_configs = vsftpdconfigread._get_parsed_configs(read_func=fileops.read,
                                                          listdir=listdir.listdir)

    assert fileops.read_called == 0
    assert not parsed_configs


def test_get_vsftpd_facts():
    directory = '/etc/vsftpd'
    file_names = ['vsftpd.conf', 'foo.conf', 'bar.conf']
    listdir = MockListDir(directory, file_names)
    fileops = MockFileOperations()
    fileops.files[os.path.join(directory, file_names[0])] = 'anonymous_enable=YES\n' \
                                                            'ca_certs_file=/foo/bar\n'
    fileops.files[os.path.join(directory, file_names[1])] = 'anonymous_enable=NO\n' \
                                                            'tcp_wrappers=YES\n'
    fileops.files[os.path.join(directory, file_names[2])] = 'strict_ssl_read_eof=yes\n' \
                                                            'tcp_wrappers=no\n'

    facts = vsftpdconfigread.get_vsftpd_facts(read_func=fileops.read, listdir=listdir.listdir)

    assert facts.default_config_hash == '892bae7b69eb66ec16afe842a15e53a5242155a4'
    assert len(facts.configs) == 3
    used_indices = set()
    for config in facts.configs:
        assert os.path.dirname(config.path) == directory
        file_name = os.path.basename(config.path)
        ix = file_names.index(file_name)
        if ix in used_indices:
            assert False
        used_indices.add(ix)
        if ix == 0:
            assert config.strict_ssl_read_eof is None
            assert config.tcp_wrappers is None
        elif ix == 1:
            assert config.strict_ssl_read_eof is None
            assert config.tcp_wrappers is True
        elif ix == 2:
            assert config.strict_ssl_read_eof is True
            assert config.tcp_wrappers is False
        else:
            assert False


def test_get_vsftpd_facts_empty_dir():
    listdir = MockListDir('/etc/vsftpd', [])
    fileops = MockFileOperations()

    facts = vsftpdconfigread.get_vsftpd_facts(read_func=fileops.read, listdir=listdir.listdir)

    assert facts.default_config_hash is None
    assert not facts.configs


def test_get_vsftpd_facts_nonexistent_dir():
    listdir = MockListDir(to_raise=make_OSError(errno.ENOENT))
    fileops = MockFileOperations()

    facts = vsftpdconfigread.get_vsftpd_facts(read_func=fileops.read, listdir=listdir.listdir)

    assert facts.default_config_hash is None
    assert not facts.configs


def test_get_vsftpd_facts_inaccessible_dir():
    listdir = MockListDir(to_raise=make_OSError(errno.EACCES))
    fileops = MockFileOperations()

    facts = vsftpdconfigread.get_vsftpd_facts(read_func=fileops.read, listdir=listdir.listdir)

    assert facts.default_config_hash is None
    assert not facts.configs


def test_is_processable_vsftpd_installed():
    installed_rpms = [
        RPM(name='sendmail', version='8.14.7', release='5.el7', epoch='0',
            packager='foo', arch='x86_64', pgpsig='bar'),
        RPM(name='vsftpd', version='3.0.2', release='25.el7', epoch='0',
            packager='foo', arch='x86_64', pgpsig='bar'),
        RPM(name='postfix', version='2.10.1', release='7.el7', epoch='0',
            packager='foo', arch='x86_64', pgpsig='bar')]
    installed_rpm_facts = InstalledRedHatSignedRPM(items=installed_rpms)

    res = vsftpdconfigread.is_processable(installed_rpm_facts)

    assert res is True


def test_is_processable_vsftpd_not_installed():
    installed_rpms = [
        RPM(name='sendmail', version='8.14.7', release='5.el7', epoch='0',
            packager='foo', arch='x86_64', pgpsig='bar'),
        RPM(name='postfix', version='2.10.1', release='7.el7', epoch='0',
            packager='foo', arch='x86_64', pgpsig='bar')]
    installed_rpm_facts = InstalledRedHatSignedRPM(items=installed_rpms)

    res = vsftpdconfigread.is_processable(installed_rpm_facts)

    assert res is False


# repos/system_upgrade/el7toel8/actors/vsftpdconfigupdate/libraries/vsftpdconfigupdate.py

import re

from leapp.libraries.common.vsftpdutils import get_default_config_hash, \
    STRICT_SSL_READ_EOF, TCP_WRAPPERS, \
    VSFTPD_DEFAULT_CONFIG_PATH
from leapp.libraries.stdlib import api

ANONYMOUS_ENABLE = 'anonymous_enable'


class FileOperations(object):
    def read(self, path):
        with open(path, 'r') as f:
            return f.read()

    def write(self, path, content):
        with open(path, 'w') as f:
            f.write(content)


def _replace_in_config(config_lines, option, value):
    res = []
    for line in config_lines:
        if re.match(r'^\s*' + option, line) is None:
            res.append(line)
        else:
            res.append('# Commented out by Leapp:')
            res.append('#' + line)
    if value is not None:
        res.append('# Added by Leapp:')
        res.append('%s=%s' % (option, value))
    return res


def _restore_default_config_file(fileops=FileOperations()):
    try:
        content = fileops.read(VSFTPD_DEFAULT_CONFIG_PATH)
    except IOError as e:
        api.current_logger().warning('Failed to read vsftpd configuration file: %s' % e)
        return
    lines = content.split('\n')
    lines = _replace_in_config(lines, ANONYMOUS_ENABLE, 'YES')
    content = '\n'.join(lines)
    content += '\n'
    fileops.write(VSFTPD_DEFAULT_CONFIG_PATH, content)


def _migrate_config(config, fileops=FileOperations()):
    if not config.tcp_wrappers and config.strict_ssl_read_eof is not None:
        return
    try:
        content = fileops.read(config.path)
    except IOError as e:
        api.current_logger().warning('Failed to read vsftpd configuration file %s: %s'
                                     % (config.path, e))
        return
    lines = content.split('\n')
    if config.tcp_wrappers:
        lines = _replace_in_config(lines, TCP_WRAPPERS, 'NO')
    if config.strict_ssl_read_eof is None:
        lines = _replace_in_config(lines, STRICT_SSL_READ_EOF, 'NO')
    content = '\n'.join(lines)
    content += '\n'
    try:
        fileops.write(config.path, content)
    except IOError as e:
        api.current_logger().warning('Failed to write vsftpd configuration file %s: %s'
                                     % (config.path, e))


def migrate_configs(facts, fileops=FileOperations()):
    if facts.default_config_hash is not None:
        new_hash = get_default_config_hash(read_func=fileops.read)
        # If the default config file was unmodified, it got replaced during the RPM upgrade,
        # so we have to change it back.
        if facts.default_config_hash != new_hash:
            _restore_default_config_file(fileops=fileops)
    for config in facts.configs:
        _migrate_config(config, fileops=fileops)


# repos/system_upgrade/el7toel8/actors/vsftpdconfigupdate/tests/test_library_vsftpdconfigupdate.py

import errno

from leapp.libraries.actor.vsftpdconfigupdate import migrate_configs
from leapp.libraries.common.testutils import make_IOError
from leapp.libraries.common.vsftpdutils import VSFTPD_DEFAULT_CONFIG_PATH
from leapp.models import VsftpdConfig, VsftpdFacts


class MockFileOperations(object):
    def __init__(self):
        self.files = {}
        self.files_read = {}
        self.files_written = {}
        self.read_called = 0
        self.write_called = 0

    def _increment_read_counters(self, path):
        self.read_called += 1
        self.files_read.setdefault(path, 0)
        self.files_read[path] += 1

    def read(self, path):
        self._increment_read_counters(path)
        try:
            return self.files[path]
        except KeyError:
            raise make_IOError(errno.ENOENT)

    def _increment_write_counters(self, path):
        self.write_called += 1
        self.files_written.setdefault(path, 0)
        self.files_written[path] += 1

    def write(self, path, content):
        self._increment_write_counters(path)
        self.files[path] = content


def test_restoring_default_config():
    content = 'anonymous_enable=NO\n' \
              'tcp_wrappers=NO\n' \
              'strict_ssl_read_eof=NO\n'
    fileops = MockFileOperations()
    fileops.files[VSFTPD_DEFAULT_CONFIG_PATH] = content
    config = VsftpdConfig(path=VSFTPD_DEFAULT_CONFIG_PATH,
                          tcp_wrappers=False, strict_ssl_read_eof=False)
    facts = VsftpdFacts(default_config_hash='foobar', configs=[config])

    migrate_configs(facts, fileops=fileops)

    assert len(fileops.files_read) == 1
    assert VSFTPD_DEFAULT_CONFIG_PATH in fileops.files_read
    assert len(fileops.files_written) == 1
    assert VSFTPD_DEFAULT_CONFIG_PATH in fileops.files_written
    expected_lines = ['# Commented out by Leapp:',
                      '#anonymous_enable=NO',
                      'tcp_wrappers=NO',
                      'strict_ssl_read_eof=NO',
                      '',
                      '# Added by Leapp:',
                      'anonymous_enable=YES',
                      '']
    assert fileops.files[VSFTPD_DEFAULT_CONFIG_PATH] == '\n'.join(expected_lines)


def test_setting_tcp_wrappers():
    path = '/etc/vsftpd/foo.conf'
    content = 'tcp_wrappers=YES\n' \
              'strict_ssl_read_eof=NO\n'
    fileops = MockFileOperations()
    fileops.files[path] = content
    config = VsftpdConfig(path=path,
                          tcp_wrappers=True, strict_ssl_read_eof=False)
    facts = VsftpdFacts(configs=[config])

    migrate_configs(facts, fileops=fileops)

    assert path in fileops.files_read
    assert len(fileops.files_written) == 1
    assert path in fileops.files_written
    expected_lines = ['# Commented out by Leapp:',
                      '#tcp_wrappers=YES',
                      'strict_ssl_read_eof=NO',
                      '',
                      '# Added by Leapp:',
                      'tcp_wrappers=NO',
                      '']
    assert fileops.files[path] == '\n'.join(expected_lines)


def test_setting_strict_ssl_read_eof():
    path = '/etc/vsftpd/bar.conf'
    content = 'local_enable=YES\n'
    fileops = MockFileOperations()
    fileops.files[path] = content
    config = VsftpdConfig(path=path,
                          tcp_wrappers=None, strict_ssl_read_eof=None)
    facts = VsftpdFacts(configs=[config])

    migrate_configs(facts, fileops=fileops)

    assert path in fileops.files_read
    assert len(fileops.files_written) == 1
    assert path in fileops.files_written
    expected_lines = ['local_enable=YES',
                      '',
                      '# Added by Leapp:',
                      'strict_ssl_read_eof=NO',
                      '']
    assert fileops.files[path] == '\n'.join(expected_lines)


# repos/system_upgrade/el7toel8/actors/ziplcheckbootentries/libraries/ziplcheckbootentries.py

from collections import defaultdict

from leapp import reporting
from leapp.libraries.common.config import architecture

FMT_LIST_SEPARATOR = '\n    - '
ZIPL_CONFIG_PATH = '/etc/zipl.conf'


def is_rescue_entry(boot_entry):
    """
    Determines whether the given boot entry is rescue.

    :param BootEntry boot_entry: Boot entry to assess
    :return: True is the entry is rescue
    :rtype:  bool
    """
    return 'rescue' in boot_entry.kernel_image.lower()


def inhibit_if_multiple_zipl_rescue_entries_present(bootloader_config):
    """
    Inhibits the upgrade if we are running on s390x and the bootloader configuration
    contains multiple rescue boot entries.

    A boot entry is recognized as a rescue entry when its title contains the `rescue` substring.

    :param SourceBootloaderConfiguration bootloader_config: The configuration of the source boot loader.
    """

    # Keep the whole information about boot entries not just their count as
    # we want to provide user with the details
    rescue_entries = []
    for boot_entry in bootloader_config.entries:
        if is_rescue_entry(boot_entry):
            rescue_entries.append(boot_entry)

    if len(rescue_entries) > 1:
        # Prepare the list of available rescue entries for user
        rescue_entries_text = ''
        for rescue_entry in rescue_entries:
            rescue_entries_text += '{0}{1}'.format(FMT_LIST_SEPARATOR, rescue_entry.title)

        summary = ('The Zipl configuration file {0} contains multiple rescue boot entries preventing migration '
                   'to BLS. Problematic entries: {1}')

        reporting.create_report([
            reporting.Title('Multiple rescue boot entries present in the bootloader configuration.'),
            reporting.Summary(summary.format(ZIPL_CONFIG_PATH, rescue_entries_text)),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.BOOT]),
            reporting.Remediation(hint='Remove rescue boot entries from the configuration and leave just one.'),
            reporting.Flags([reporting.Flags.INHIBITOR])
        ])


def extract_kernel_version(kernel_img_path):
    """
    Extracts the kernel version out of the given image path.

    The extraction logic is designed to closely mimick the logic Zipl configuration to BLS
    conversion script works, so that it is possible to identify the possible issues with kernel
    images.

    :param str kernel_img_path: The path to the kernel image.
    :returns: Extracted kernel version from the given path
    :rtype: str
    """

    # Mimick bash substitution used in the conversion script, see:
    # https://github.com/ibm-s390-linux/s390-tools/blob/b5604850ab66f862850568a37404faa647b5c098/scripts/zipl-switch-to-blscfg#L168
    if 'vmlinuz-' in kernel_img_path:
        fragments = kernel_img_path.rsplit('/vmlinuz-', 1)
        return fragments[1] if len(fragments) > 1 else fragments[0]

    fragments = kernel_img_path.rsplit('/', 1)
    return fragments[1] if len(fragments) > 1 else fragments[0]


def inhibit_if_entries_share_kernel_version(bootloader_config):
    """
    Inhibits the upgrade if there are boot entries sharing the same kernel image version.

    The logic of identification whether the images are the same mimics the zipl-switch-to-blscfg, as it fails
    to perform the conversion if there are entries with the same kernel image.

    :param SourceBootloaderConfiguration bootloader_config: The configuration of the source boot loader.
    """

    used_kernel_versions = defaultdict(list)  # Maps images to the boot entries in which they are used
    for boot_entry in bootloader_config.entries:
        if is_rescue_entry(boot_entry):
            # Rescue entries are handled differently and their images should not cause naming collisions
            continue

        kernel_version = extract_kernel_version(boot_entry.kernel_image)
        used_kernel_versions[kernel_version].append(boot_entry)

    versions_used_multiple_times = []
    for version, version_boot_entries in used_kernel_versions.items():
        if len(version_boot_entries) > 1:
            # Keep the information about entries for the report
            versions_used_multiple_times.append((version, version_boot_entries))

    if versions_used_multiple_times:
        problematic_entries_details = ''
        for version, version_boot_entries in versions_used_multiple_times:
            entry_titles = ['"{0}"'.format(entry.title) for entry in version_boot_entries]
            problematic_entries_details += '{0}{1} (found in entries: {2})'.format(
                    FMT_LIST_SEPARATOR,
                    version,
                    ', '.join(entry_titles)
            )

        summary = ('The zipl configuration file {0} contains boot entries sharing the same kernel version '
                   'preventing migration to BLS. Kernel versions shared: {1}')
        reporting.create_report([
            reporting.Title('Boot entries sharing the same kernel version found.'),
            reporting.Summary(summary.format(ZIPL_CONFIG_PATH, problematic_entries_details)),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.BOOT]),
            reporting.Remediation(
                hint='Remove boot entries sharing the same kernel version from the configuration and leave just one.'),
            reporting.Flags([reporting.Flags.INHIBITOR])
        ])


def inhibit_if_invalid_zipl_configuration(bootloader_config):
    if not architecture.matches_architecture(architecture.ARCH_S390X):
        # Zipl is used only on s390x
        return

    inhibit_if_multiple_zipl_rescue_entries_present(bootloader_config)
    inhibit_if_entries_share_kernel_version(bootloader_config)


# repos/system_upgrade/el7toel8/actors/ziplcheckbootentries/tests/test_ziplcheckbootentries.py

import pytest

from leapp import reporting
from leapp.libraries.actor import ziplcheckbootentries
from leapp.libraries.actor.ziplcheckbootentries import (
    extract_kernel_version,
    inhibit_if_entries_share_kernel_version,
    inhibit_if_invalid_zipl_configuration,
    inhibit_if_multiple_zipl_rescue_entries_present
)
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import BootEntry, SourceBootLoaderConfiguration
from leapp.snactor.fixture import current_actor_context


def test_inhibition_multiple_rescue_entries_present(monkeypatch):
    """Tests whether the upgrade process is inhibited when multiple rescue boot entries are present."""
    mocked_report = create_report_mocked()
    monkeypatch.setattr(architecture, 'matches_architecture', lambda dummy: True)
    monkeypatch.setattr(reporting, 'create_report', mocked_report)

    boot_entries = [
        BootEntry(title='entry_1', kernel_image="img"),
        BootEntry(title='entry_1_Rescue', kernel_image="img_Rescue"),
        BootEntry(title='entry_2', kernel_image="img"),
        BootEntry(title='entry_2_rescue-ver2.3', kernel_image="img_rescue"),
    ]

    inhibit_if_multiple_zipl_rescue_entries_present(SourceBootLoaderConfiguration(entries=boot_entries))

    assert mocked_report.called, 'Report should be created when multiple rescue entries are present.'

    fail_description = 'The correct rescue entries are not present in the report summary.'
    report_summary = mocked_report.report_fields['summary']
    for expected_rescue_entry in ['entry_1_Rescue', 'entry_2_rescue-ver2.3']:
        assert expected_rescue_entry in report_summary, fail_description

    fail_description = 'Upgrade should be inhibited on multiple rescue entries.'
    assert 'inhibitor' in mocked_report.report_fields['flags'], fail_description


def test_inhibition_multiple_rescue_entries_not_present(monkeypatch):
    """Tests whether the upgrade process is not inhibited when multiple rescue boot entries are not present."""
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    boot_entries = [
        BootEntry(title='entry_1', kernel_image="img"),
        BootEntry(title='entry_2', kernel_image="img"),
        BootEntry(title='entry_2_rescue-ver2.3', kernel_image="img_rescue"),
    ]

    inhibit_if_multiple_zipl_rescue_entries_present(SourceBootLoaderConfiguration(entries=boot_entries))

    assert not reporting.create_report.called, 'Report was created, even if multiple rescue entries were not present.'


def test_inhibition_when_entries_do_not_share_kernel_image(monkeypatch):
    """Tests whether the IPU is not inhibited when there are no kernel images shared between boot entries."""
    entries = [
        BootEntry(title='Linux#0', kernel_image='/boot/vmlinuz-4.17.0-240.1.1.el8_3.x86_64'),
        BootEntry(title='Linux#1', kernel_image='/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64')
    ]

    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    inhibit_if_entries_share_kernel_version(SourceBootLoaderConfiguration(entries=entries))
    assert not reporting.create_report.called


@pytest.mark.parametrize(
    ('boot_entries',),
    [([BootEntry(title='Linux0', kernel_image='/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64'),
       BootEntry(title='Linux1', kernel_image='/boot/4.18.0-240.1.1.el8_3.x86_64')],),
     ([BootEntry(title='Linux0', kernel_image='/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64'),
       BootEntry(title='Linux1', kernel_image='/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64')],)])
def test_inhibit_when_entries_share_kernel_image(monkeypatch, boot_entries):
    """Tests whether the IPU gets inhibited when there are kernel images shared between boot entries."""

    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    inhibit_if_entries_share_kernel_version(SourceBootLoaderConfiguration(entries=boot_entries))

    assert reporting.create_report.called
    assert 'inhibitor' in reporting.create_report.report_fields['flags']

    report_summary = reporting.create_report.report_fields['summary']
    assert '- 4.18.0-240.1.1.el8_3.x86_64 (found in entries: "Linux0", "Linux1")' in report_summary


@pytest.mark.parametrize(
    ('boot_entries',),
    [([BootEntry(title='Linux', kernel_image='/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64'),
       BootEntry(title='Linux-rescue', kernel_image='/boot/vmlinuz-rescue-4.18.0-240.1.1.el8_3.x86_64')],),
     ([BootEntry(title='Linux0-rescue', kernel_image='/boot/vmlinuz-rescue-4.18.0-240.1.1.el8_3.x86_64'),
       BootEntry(title='Linux1-rescue', kernel_image='/boot/vmlinuz-rescue-4.18.0-240.1.1.el8_3.x86_64')],)])
def test_inhibition_when_rescue_entries_share_kernel(monkeypatch, boot_entries):
    """
    Tests whether the IPU is not inhibited when there are kernel images with the same version shared between rescue
    boot entries.
    """
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    inhibit_if_entries_share_kernel_version(SourceBootLoaderConfiguration(entries=boot_entries))
    assert not reporting.create_report.called


@pytest.mark.parametrize(('arch',), [(arch,) for arch in architecture.ARCH_SUPPORTED])
def test_checks_performed_only_on_s390x_arch(arch, monkeypatch):
    """Tests whether the actor doesn't perform different architectures than s390x."""
    should_perform = False
    if arch == architecture.ARCH_S390X:  # Rescue entries should be checked only on s390x.
        should_perform = True

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=arch))
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    boot_entries = [BootEntry(title='rescue0', kernel_image='/boot/vmlinuz-rescue-4.18.0-240.1.1.el8_3.x86_64'),
                    BootEntry(title='rescue1', kernel_image='/boot/vmlinuz-rescue-4.19.0-240.1.1.el8_3.x86_64')]

    inhibit_if_invalid_zipl_configuration(SourceBootLoaderConfiguration(entries=boot_entries))

    fail_description = 'Rescue entries should not be checked on non s390x architecture.'
    if should_perform:
        fail_description = 'No report was created when running on s390x and multiple rescue entries were used.'
    assert bool(reporting.create_report.called) == should_perform, fail_description

    if should_perform:
        inhibitor_description = 'contains multiple rescue boot entries'
        assert inhibitor_description in reporting.create_report.report_fields['summary']

    boot_entries = [BootEntry(title='Linux1', kernel_image='/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64'),
                    BootEntry(title='Linux2', kernel_image='/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64')]

    inhibit_if_invalid_zipl_configuration(SourceBootLoaderConfiguration(entries=boot_entries))

    fail_description = 'Check for boot entries with the same kernel version should not be performed on non s390x arch.'
    if should_perform:
        fail_description = ('No report was created when running on s390x and boot entries'
                            'with the same kernel version are present')
    assert bool(reporting.create_report.called) == should_perform, fail_description
    if should_perform:
        inhibitor_description = 'contains boot entries sharing the same kernel version'
        assert inhibitor_description in reporting.create_report.report_fields['summary']


def test_extract_kernel_version():
    # Manually generated via experimentation with the zipl-switch-to-blscfg
    versions_from_img_paths = [
        ('/boot/vmlinuz-4.18.0-240.1.1.el8_3.x86_64', '4.18.0-240.1.1.el8_3.x86_64'),
        ('/boot/4.18.0-240.1.1.el8_3.x86_64', '4.18.0-240.1.1.el8_3.x86_64'),
        ('/boot/patched-4.18.0-240.1.1.el8_3.x86_64', 'patched-4.18.0-240.1.1.el8_3.x86_64'),
        ('patched-4.18.0-240.1.1.el8_3.x86_64', 'patched-4.18.0-240.1.1.el8_3.x86_64'),
    ]

    for path, version in versions_from_img_paths:
        assert extract_kernel_version(path) == version


# repos/system_upgrade/el7toel8/actors/ziplconverttoblscfg/tests/unit_test_ziplconverttoblscfg.py

def test_actor():
    pass


# repos/system_upgrade/common/files/rhel_upgrade.py

# plugin inspired by "system_upgrade.py" from rpm-software-management
from __future__ import print_function

import json
import sys

import dnf
import dnf.cli
import dnf.module.module_base

CMDS = ['check', 'download', 'dry-run', 'upgrade']
"""
Basic subcommands for the plugin.

    check    -> check we are able to calculate the DNF transaction using
                only the YUM/DNF repositories metadata (no packages downloaded)
    download -> calculate the transaction, download all packages and test
                the transaction is duable using data contained in RPMs
                (include search for file conflicts, disk space, ...)
    dry-run  -> test the transaction again with the cached content (including
                the downloaded RPMs; it is subset of the download cmd)(
                  BZ: 1976932 - not enough space on disk,
                  PR: https://github.com/oamg/leapp-repository/pull/734
                )
    upgrade  -> perform the DNF transaction using the cached data only
"""


class DoNotDownload(Exception):
    pass


def _do_not_download_packages(packages, progress=None, total=None):
    raise DoNotDownload()


class RhelUpgradeCommand(dnf.cli.Command):
    aliases = ('rhel-upgrade',)
    summary = ("Plugin for upgrading to the next RHEL major release")

    def __init__(self, cli):
        super(RhelUpgradeCommand, self).__init__(cli)
        self.plugin_data = {}

    @staticmethod
    def set_argparser(parser):
        parser.add_argument('tid', nargs=1, choices=CMDS,
                            metavar="[%s]" % "|".join(CMDS))
        parser.add_argument('filename')

    def _process_entities(self, entities, op, entity_name):
        """
        Adds list of packages for given operation to the transaction
        """
        entities_notfound = []

        for spec in entities:
            try:
                op(spec)
            except dnf.exceptions.MarkingError:
                if isinstance(spec, (list, tuple)):
                    entities_notfound.extend(spec)
                else:
                    entities_notfound.append(spec)
        if entities_notfound:
            err_str = ('{} marked by Leapp to {} not found '
                       'in repositories metadata: '.format(entity_name, op.__name__) + ' '.join(entities_notfound))
            print('Warning: ' + err_str, file=sys.stderr)

    def _save_aws_region(self, region):
        self.plugin_data['rhui']['aws']['region'] = region
        with open(self.opts.filename, 'w+') as fo:
            json.dump(self.plugin_data, fo, sort_keys=True, indent=2)

    def _read_aws_region(self, repo):
        region = None
        if repo.baseurl:
            # baseurl is tuple (changed by Amazon-id plugin)
            # here we take just the first baseurl as the REGION will be same for all of them
            region = repo.baseurl[0].split('.', 2)[1]
        elif repo.mirrorlist:
            region = repo.mirrorlist.split('.', 2)[1]
        if not region:
            print('Could not read AWS REGION from either baseurl or mirrorlist', file=sys.stderr)
            sys.exit(1)
        return region

    def _fix_rhui_url(self, repo, region):
        if repo.baseurl:
            repo.baseurl = tuple(
                url.replace('REGION', region, 1) for url in repo.baseurl
            )
        elif repo.mirrorlist:
            repo.mirrorlist = repo.mirrorlist.replace('REGION', region, 1)
        else:
            raise dnf.exceptions.RepoError("RHUI repository %s does not have an url" % repo.name)
        return repo

    def pre_configure(self):
        with open(self.opts.filename) as fo:
            self.plugin_data = json.load(fo)
        # There is an issue that ignores releasever value if it is set at configure
        self.base.conf.releasever = self.plugin_data['dnf_conf']['releasever']

    def configure(self):

        on_aws = self.plugin_data['rhui']['aws']['on_aws']
        self.cli.demands.root_user = True
        self.cli.demands.resolving = self.opts.tid[0] != 'check'
        self.cli.demands.available_repos = True
        self.cli.demands.sack_activation = True
        self.cli.demands.cacheonly = self.opts.tid[0] in ['dry-run', 'upgrade']
        self.cli.demands.allow_erasing = self.plugin_data['dnf_conf']['allow_erasing']
        self.base.conf.protected_packages = []
        self.base.conf.best = self.plugin_data['dnf_conf']['best']
        self.base.conf.assumeyes = True
        self.base.conf.gpgcheck = self.plugin_data['dnf_conf']['gpgcheck']
        self.base.conf.debug_solver = self.plugin_data['dnf_conf']['debugsolver']
        self.base.conf.module_platform_id = self.plugin_data['dnf_conf']['platform_id']
        installroot = self.plugin_data['dnf_conf'].get('installroot')
        if installroot:
            self.base.conf.installroot = installroot
        if self.plugin_data['dnf_conf']['test_flag'] and self.opts.tid[0] in ['download', 'dry-run']:
            self.base.conf.tsflags.append("test")

        enabled_repos = self.plugin_data['dnf_conf']['enable_repos']
        self.base.repos.all().disable()

        aws_region = None

        for repo in self.base.repos.all():
            if repo.id in enabled_repos:
                repo.skip_if_unavailable = False
                if not self.base.conf.gpgcheck:
                    repo.gpgcheck = False
                repo.enable()
                if self.opts.tid[0] == 'download' and on_aws:
                    # during the upgrade phase we has to disable "Amazon-id" plugin as we do not have networking
                    # in initramdisk (yet, but we probably do not want it to do anything anyway as we already have
                    # packages downloaded and cached). However, when we disable it, the plugin cannot substitute
                    # "REGION" placeholder in mirrorlist url and consequently we cannot identify a correct cache
                    # folder in "/var/cache/dnf" as it has different digest calculated based on already substituted
                    # placeholder.
                    # E.g
                    # "https://rhui3.REGION.aws.ce.redhat.com" becames "https://rhui3.eu-central-1.aws.ce.redhat.com"
                    #
                    # region should be same for all repos so we are fine to collect it from
                    # the last one
                    aws_region = self._read_aws_region(repo)
                if self.opts.tid[0] in ['dry-run', 'upgrade'] and on_aws:
                    aws_region = self.plugin_data['rhui']['aws']['region']
                    if aws_region:
                        repo = self._fix_rhui_url(repo, aws_region)

        if aws_region and self.opts.tid[0] == 'download':
            self._save_aws_region(aws_region)

    def run(self):
        # takes local rpms, creates Package objects from them, and then adds them to the sack as virtual repository
        local_rpm_objects = self.base.add_remote_rpms(self.plugin_data['pkgs_info']['local_rpms'])

        for pkg in local_rpm_objects:
            self.base.package_install(pkg)

        module_base = dnf.module.module_base.ModuleBase(self.base)

        # Module tasks
        modules_to_enable = self.plugin_data['pkgs_info'].get('modules_to_enable', ())

        # Package tasks
        to_install = self.plugin_data['pkgs_info']['to_install']
        to_remove = self.plugin_data['pkgs_info']['to_remove']
        to_upgrade = self.plugin_data['pkgs_info']['to_upgrade']

        # Modules to enable
        self._process_entities(entities=[modules_to_enable], op=module_base.enable, entity_name='Module stream')

        # Packages to be removed
        self._process_entities(entities=to_remove, op=self.base.remove, entity_name='Package')
        # Packages to be installed
        self._process_entities(entities=to_install, op=self.base.install, entity_name='Package')
        # Packages to be upgraded
        self._process_entities(entities=to_upgrade, op=self.base.upgrade, entity_name='Package')

        self.base.distro_sync()

        if self.opts.tid[0] == 'check':
            try:
                self.base.resolve(allow_erasing=self.cli.demands.allow_erasing)
            except dnf.exceptions.DepsolveError as e:
                print('Transaction check: ', file=sys.stderr)
                print(str(e), file=sys.stderr)
                raise

            # We are doing this to avoid downloading the packages in the check phase
            self.base.download_packages = _do_not_download_packages
            try:
                displays = []
                if self.cli.demands.transaction_display is not None:
                    displays.append(self.cli.demands.transaction_display)
                self.base.do_transaction(display=displays)
            except DoNotDownload:
                print('Check completed.')


class RhelUpgradePlugin(dnf.Plugin):
    name = 'rhel-upgrade'

    def __init__(self, base, cli):
        super(RhelUpgradePlugin, self).__init__(base, cli)
        if cli:
            cli.register_command(RhelUpgradeCommand)


# repos/system_upgrade/common/libraries/dnfconfig.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import CalledProcessError, api


def get_leapp_packages():
    """
    Return the list of leapp and leapp-repository rpms that should be preserved
    during the upgrade.

    It's list of packages that should be preserved, not what is really
    installed.

    The snactor RPM doesn't have to be installed, but if so, we have to take
    care about that too as well to preven broken dnf transaction.
    """
    # TODO: should we set the seatbelt and exclude leapp RPMs from the target
    # system too?
    generic = ['leapp', 'snactor']
    if get_source_major_version() == '7':
        return generic + ['python2-leapp', 'leapp-upgrade-el7toel8']

    return generic + ['python3-leapp', 'leapp-upgrade-el8toel9']


def _strip_split(data, sep, maxsplit=-1):
    """
    Just like str.split(), but remove ambient whitespaces from all items
    """
    return [item.strip() for item in data.split(sep, maxsplit)]


def _get_main_dump(context):
    """
    Return the dnf configuration dump of main options for the given context.

    Returns the list of lines after the line with "[main]" section
    """

    try:
        data = context.call(['dnf', 'config-manager', '--dump'], split=True)['stdout']
    except CalledProcessError as e:
        api.current_logger().error('Cannot obtain the dnf configuration')
        raise StopActorExecutionError(
            message='Cannot obtain data about the DNF configuration',
            details={'stdout': e.stdout, 'stderr': e.stderr}
        )

    try:
        # return index of the first item in the main section
        main_start = data.index('[main]') + 1
    except ValueError:
        raise StopActorExecutionError(
            message='Invalid DNF configuration data (missing [main])',
            details=data,
        )

    output_data = {}
    for line in data[main_start:]:
        try:
            key, val = _strip_split(line, '=', 1)
        except ValueError:
            # This is not expected to happen, but call it a seatbelt in case
            # the dnf dump implementation will change and we will miss it
            # This is not such a hard error as the one above, as it means
            # some values could be incomplete, however we are still able
            # to continue.
            api.current_logger().warning(
                'Cannot parse the dnf dump correctly, line: {}'.format(line))
            pass
        output_data[key] = val

    return output_data


def _get_excluded_pkgs(context):
    """
    Return the list of excluded packages for DNF in the given context.

    It shouldn't be used on the source system. It is expected this functions
    is called only in the target userspace container or on the target system.
    """
    pkgs = _strip_split(_get_main_dump(context).get('exclude', ''), ',')
    return [i for i in pkgs if i]


def _set_excluded_pkgs(context, pkglist):
    """
    Configure DNF to exclude packages in the given list

    Raise the CalledProcessError on error.
    """
    exclude = 'exclude={}'.format(','.join(pkglist))
    cmd = ['dnf', 'config-manager', '--save', '--setopt', exclude]

    try:
        context.call(cmd)
    except CalledProcessError:
        api.current_logger().error('Cannot set the dnf configuration')
        raise
    api.current_logger().debug('The DNF configuration has been updated to exclude leapp packages.')


def exclude_leapp_rpms(context):
    """
    Ensure the leapp RPMs are excluded from any DNF transaction.

    This has to be called several times to ensure that our RPMs are not removed
    or updated (replaced) during the IPU. The action should happen inside
        - the target userspace container
        - on the host system
    So user will have to drop these packages from the exclude after the
    upgrade.
    """
    to_exclude = list(set(_get_excluded_pkgs(context) + get_leapp_packages()))
    _set_excluded_pkgs(context, to_exclude)


# repos/system_upgrade/common/libraries/dnfplugin.py

import contextlib
import itertools
import json
import os
import shutil

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import dnfconfig, guards, mounting, overlaygen, rhsm, utils
from leapp.libraries.common.config.version import (
    get_source_major_version,
    get_target_major_version,
    get_target_version
)
from leapp.libraries.stdlib import api, CalledProcessError, config

DNF_PLUGIN_NAME = 'rhel_upgrade.py'


class _DnfPluginPathStr(str):
    _PATHS = {
        "8": os.path.join('/lib/python3.6/site-packages/dnf-plugins', DNF_PLUGIN_NAME),
        "9": os.path.join('/lib/python3.9/site-packages/dnf-plugins', DNF_PLUGIN_NAME),
    }

    def __init__(self):  # noqa: W0231; pylint: disable=super-init-not-called
        self.data = ""

    def _feed(self):
        major = get_target_major_version()
        if major not in _DnfPluginPathStr._PATHS:
            raise KeyError('{} is not a supported target version of RHEL'.format(major))
        self.data = _DnfPluginPathStr._PATHS[major]

    def __str__(self):
        self._feed()
        return str(self.data)

    def __repr__(self):
        self._feed()
        return repr(self.data)

    def lstrip(self, chars=None):
        self._feed()
        return self.data.lstrip(chars)


# Deprecated
DNF_PLUGIN_PATH = _DnfPluginPathStr()

DNF_PLUGIN_DATA_NAME = 'dnf-plugin-data.txt'
DNF_PLUGIN_DATA_PATH = os.path.join('/var/lib/leapp', DNF_PLUGIN_DATA_NAME)
DNF_PLUGIN_DATA_LOG_PATH = os.path.join('/var/log/leapp', DNF_PLUGIN_DATA_NAME)
DNF_DEBUG_DATA_PATH = '/var/log/leapp/dnf-debugdata/'


def install(target_basedir):
    """
    Installs our plugin to the DNF plugins.
    """
    try:
        shutil.copy2(
            api.get_file_path(DNF_PLUGIN_NAME),
            os.path.join(target_basedir, DNF_PLUGIN_PATH.lstrip('/')))
    except EnvironmentError as e:
        api.current_logger().debug('Failed to install DNF plugin', exc_info=True)
        raise StopActorExecutionError(
            message='Failed to install DNF plugin. Error: {}'.format(str(e))
        )


def _rebuild_rpm_db(context, root=None):
    """
    Convert rpmdb from BerkeleyDB to Sqlite
    """
    base_cmd = ['rpmdb', '--rebuilddb']
    cmd = base_cmd if not root else base_cmd + ['-r', root]
    context.call(cmd)


def build_plugin_data(target_repoids, debug, test, tasks, on_aws):
    """
    Generates a dictionary with the DNF plugin data.
    """
    # get list of repo IDs of target repositories that should be used for upgrade
    data = {
        'pkgs_info': {
            'local_rpms': [os.path.join('/installroot', pkg.lstrip('/')) for pkg in tasks.local_rpms],
            'to_install': tasks.to_install,
            'to_remove': tasks.to_remove,
            'to_upgrade': tasks.to_upgrade,
            'modules_to_enable': ['{}:{}'.format(m.name, m.stream) for m in tasks.modules_to_enable],
        },
        'dnf_conf': {
            'allow_erasing': True,
            'best': True,
            'debugsolver': debug,
            'disable_repos': True,
            'enable_repos': target_repoids,
            'gpgcheck': False,
            'platform_id': 'platform:el{}'.format(get_target_major_version()),
            'releasever': get_target_version(),
            'installroot': '/installroot',
            'test_flag': test
        },
        'rhui': {
            'aws': {
              'on_aws': on_aws,
              'region': None,
            }
        }
    }
    return data


def create_config(context, target_repoids, debug, test, tasks, on_aws=False):
    """
    Creates the configuration data file for our DNF plugin.
    """
    context.makedirs(os.path.dirname(DNF_PLUGIN_DATA_PATH), exists_ok=True)
    with context.open(DNF_PLUGIN_DATA_PATH, 'w+') as f:
        config_data = build_plugin_data(
            target_repoids=target_repoids, debug=debug, test=test, tasks=tasks, on_aws=on_aws
        )
        json.dump(config_data, f, sort_keys=True, indent=2)


def backup_config(context):
    """
    Backs up the configuration data used for the plugin.
    """
    context.copy_from(DNF_PLUGIN_DATA_PATH, DNF_PLUGIN_DATA_LOG_PATH)


def backup_debug_data(context):
    """
    Performs the backup of DNF debug data
    """
    if config.is_debug():
        # The debugdata is a folder generated by dnf when using the --debugsolver dnf option. We switch on the
        # debug_solver dnf config parameter in our rhel-upgrade dnf plugin when LEAPP_DEBUG env var set to 1.
        try:
            context.copytree_from('/debugdata', DNF_DEBUG_DATA_PATH)
        except OSError as e:
            api.current_logger().warning('Failed to copy debugdata. Message: {}'.format(str(e)), exc_info=True)


def _transaction(context, stage, target_repoids, tasks, plugin_info, test=False, cmd_prefix=None, on_aws=False):
    """
    Perform the actual DNF rpm download via our DNF plugin
    """

    # we do not want
    if stage not in ['dry-run', 'upgrade']:
        create_config(
            context=context,
            target_repoids=target_repoids,
            debug=config.is_debug(),
            test=test, tasks=tasks,
            on_aws=on_aws
        )
    backup_config(context=context)

    # FIXME: rhsm
    with guards.guarded_execution(guards.connection_guard(), guards.space_guard()):
        cmd_prefix = cmd_prefix or []
        common_params = []
        if config.is_verbose():
            common_params.append('-v')
        if rhsm.skip_rhsm():
            common_params += ['--disableplugin', 'subscription-manager']
        if plugin_info:
            for info in plugin_info:
                if stage in info.disable_in:
                    common_params += ['--disableplugin', info.name]
        env = {}
        if get_target_major_version() == '9':
            # allow handling new RHEL 9 syscalls by systemd-nspawn
            env = {'SYSTEMD_SECCOMP': '0'}

            # We need to reset modules twice, once before we check, and the second time before we actually perform
            # the upgrade. Not more often as the modules will be reset already.
            if stage in ('check', 'upgrade') and tasks.modules_to_reset:
                # We shall only reset modules that are not going to be enabled
                # This will make sure it is so
                modules_to_reset = {(module.name, module.stream) for module in tasks.modules_to_reset}
                modules_to_enable = {(module.name, module.stream) for module in tasks.modules_to_enable}
                module_reset_list = [module[0] for module in modules_to_reset - modules_to_enable]
                # Perform module reset
                cmd = ['/usr/bin/dnf', 'module', 'reset', '--enabled', ] + module_reset_list
                cmd += ['--disablerepo', '*', '-y', '--installroot', '/installroot']
                try:
                    context.call(
                        cmd=cmd_prefix + cmd + common_params,
                        callback_raw=utils.logging_handler,
                        env=env
                    )
                except (CalledProcessError, OSError):
                    api.current_logger().debug('Failed to reset modules via dnf with an error. Ignoring.',
                                               exc_info=True)

        cmd = [
            '/usr/bin/dnf',
            'rhel-upgrade',
            stage,
            DNF_PLUGIN_DATA_PATH
        ]
        try:
            context.call(
                cmd=cmd_prefix + cmd + common_params,
                callback_raw=utils.logging_handler,
                env=env
            )
        except OSError as e:
            api.current_logger().error('Could not call dnf command: Message: %s', str(e), exc_info=True)
            raise StopActorExecutionError(
                message='Failed to execute dnf. Reason: {}'.format(str(e))
            )
        except CalledProcessError as e:
            api.current_logger().error('DNF execution failed: ')
            raise StopActorExecutionError(
                message='DNF execution failed with non zero exit code.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}'.format(
                    stdout=e.stdout, stderr=e.stderr)
            )
        finally:
            if stage == 'check':
                backup_debug_data(context=context)


@contextlib.contextmanager
def _prepare_transaction(used_repos, target_userspace_info, binds=()):
    """ Creates the transaction environment needed for the target userspace DNF execution  """
    target_repoids = set()
    for message in used_repos:
        target_repoids.update([repo.repoid for repo in message.repos])
    with mounting.NspawnActions(base_dir=target_userspace_info.path, binds=binds) as context:
        yield context, list(target_repoids), target_userspace_info


def _apply_yum_workaround(context):
    """
    Apply the yum workaround in the given context environment if on RHEL 7.
    """
    if get_source_major_version() == '7':
        utils.apply_yum_workaround(context)


def install_initramdisk_requirements(packages, target_userspace_info, used_repos):
    """
    Performs the installation of packages into the initram disk
    """
    with _prepare_transaction(used_repos=used_repos,
                              target_userspace_info=target_userspace_info) as (context, target_repoids, _unused):
        if get_target_major_version() == '9':
            _rebuild_rpm_db(context)
        repos_opt = [['--enablerepo', repo] for repo in target_repoids]
        repos_opt = list(itertools.chain(*repos_opt))
        cmd = [
            'dnf',
            'install',
            '-y',
            '--nogpgcheck',
            '--setopt=module_platform_id=platform:el{}'.format(get_target_major_version()),
            '--setopt=keepcache=1',
            '--releasever', api.current_actor().configuration.version.target,
            '--disablerepo', '*'
        ] + repos_opt + list(packages)
        if config.is_verbose():
            cmd.append('-v')
        if rhsm.skip_rhsm():
            cmd += ['--disableplugin', 'subscription-manager']
        env = {}
        if get_target_major_version() == '9':
            # allow handling new RHEL 9 syscalls by systemd-nspawn
            env = {'SYSTEMD_SECCOMP': '0'}
        context.call(cmd, env=env)


def perform_transaction_install(target_userspace_info, storage_info, used_repos, tasks, plugin_info):
    """
    Performs the actual installation with the DNF rhel-upgrade plugin using the target userspace
    """

    # These bind mounts are performed by systemd-nspawn --bind parameters
    bind_mounts = [
        '/:/installroot',
        '/dev:/installroot/dev',
        '/proc:/installroot/proc',
        '/run/udev:/installroot/run/udev',
    ]

    if get_target_major_version() == '8':
        bind_mounts.append('/sys:/installroot/sys')
    else:
        # the target major version is RHEL 9+
        # we are bindmounting host's "/sys" to the intermediate "/hostsys"
        # in the upgrade initramdisk to avoid cgroups tree layout clash
        bind_mounts.append('/hostsys:/installroot/sys')

    already_mounted = {entry.split(':')[0] for entry in bind_mounts}
    for entry in storage_info.fstab:
        mp = entry.fs_file
        if not os.path.isdir(mp):
            continue
        if mp not in already_mounted:
            bind_mounts.append('{}:{}'.format(mp, os.path.join('/installroot', mp.lstrip('/'))))

    if os.path.ismount('/boot'):
        bind_mounts.append('/boot:/installroot/boot')

    if os.path.ismount('/boot/efi'):
        bind_mounts.append('/boot/efi:/installroot/boot/efi')

    with _prepare_transaction(used_repos=used_repos,
                              target_userspace_info=target_userspace_info,
                              binds=bind_mounts
                              ) as (context, target_repoids, _unused):
        # the below nsenter command is important as we need to enter sysvipc namespace on the host so we can
        # communicate with udev
        cmd_prefix = ['nsenter', '--ipc=/installroot/proc/1/ns/ipc']

        # we have to ensure the leapp packages will stay untouched
        # Note: this is the most probably duplicate action - it should be already
        # set like that, however seatbelt is a good thing.
        dnfconfig.exclude_leapp_rpms(context)

        if get_target_major_version() == '9':
            _rebuild_rpm_db(context, root='/installroot')
        _transaction(
            context=context, stage='upgrade', target_repoids=target_repoids, plugin_info=plugin_info, tasks=tasks,
            cmd_prefix=cmd_prefix
        )

        # we have to ensure the leapp packages will stay untouched even after the
        # upgrade is fully finished (it cannot be done before the upgarde
        # on the host as the config-manager plugin is available since rhel-8)
        dnfconfig.exclude_leapp_rpms(mounting.NotIsolatedActions(base_dir='/'))


@contextlib.contextmanager
def _prepare_perform(used_repos, target_userspace_info, xfs_info, storage_info):
    with _prepare_transaction(used_repos=used_repos,
                              target_userspace_info=target_userspace_info
                              ) as (context, target_repoids, userspace_info):
        with overlaygen.create_source_overlay(mounts_dir=userspace_info.mounts, scratch_dir=userspace_info.scratch,
                                              xfs_info=xfs_info, storage_info=storage_info,
                                              mount_target=os.path.join(context.base_dir, 'installroot')) as overlay:
            yield context, overlay, target_repoids


def perform_transaction_check(target_userspace_info, used_repos, tasks, xfs_info, storage_info, plugin_info):
    """
    Perform DNF transaction check using our plugin
    """
    with _prepare_perform(used_repos=used_repos, target_userspace_info=target_userspace_info, xfs_info=xfs_info,
                          storage_info=storage_info) as (context, overlay, target_repoids):
        _apply_yum_workaround(overlay.nspawn())
        dnfconfig.exclude_leapp_rpms(context)
        _transaction(
            context=context, stage='check', target_repoids=target_repoids, plugin_info=plugin_info, tasks=tasks
        )


def perform_rpm_download(target_userspace_info, used_repos, tasks, xfs_info, storage_info, plugin_info, on_aws=False):
    """
    Perform RPM download including the transaction test using dnf with our plugin
    """
    with _prepare_perform(used_repos=used_repos, target_userspace_info=target_userspace_info, xfs_info=xfs_info,
                          storage_info=storage_info) as (context, overlay, target_repoids):
        _apply_yum_workaround(overlay.nspawn())
        dnfconfig.exclude_leapp_rpms(context)
        _transaction(
            context=context, stage='download', target_repoids=target_repoids, plugin_info=plugin_info, tasks=tasks,
            test=True, on_aws=on_aws
        )


def perform_dry_run(target_userspace_info, used_repos, tasks, xfs_info, storage_info, plugin_info, on_aws=False):
    """
    Perform the dnf transaction test / dry-run using only cached data.
    """
    with _prepare_perform(used_repos=used_repos, target_userspace_info=target_userspace_info, xfs_info=xfs_info,
                          storage_info=storage_info) as (context, overlay, target_repoids):
        _apply_yum_workaround(overlay.nspawn())
        _transaction(
            context=context, stage='dry-run', target_repoids=target_repoids, plugin_info=plugin_info, tasks=tasks,
            test=True, on_aws=on_aws
        )


# repos/system_upgrade/common/libraries/efi_reboot_fix.py

from re import compile as regexp
import os

from leapp.libraries.stdlib import run


_current_boot_matcher = regexp(r'BootCurrent: (?P<boot_current>([0-9A-F]*))')
_next_boot_matcher = regexp(r'BootNext: (?P<boot_next>([0-9A-F]*))')


def get_current_boot_match(string):
    match = _current_boot_matcher.match(string)
    if not match:
        return None
    captured_groups = match.groupdict()
    return captured_groups['boot_current']


def get_next_boot_match(string):
    match = _next_boot_matcher.match(string)
    if not match:
        return None
    captured_groups = match.groupdict()
    return captured_groups['boot_next']


def maybe_emit_updated_boot_entry():
    if not os.path.exists('/sbin/efibootmgr'):
        return

    efi_info = run(['/sbin/efibootmgr'], checked=False, split=True)
    if efi_info['exit_code'] != 0:
        # Not an efi system
        return

    current_boot, next_boot = None, None
    for line in efi_info['stdout']:
        current_match = get_current_boot_match(line)
        if current_match:
            current_boot = current_match

        next_match = get_next_boot_match(line)
        if next_match:
            next_boot = next_match

    if current_boot and not next_boot:
        # We set BootNext to CurrentBoot only if BootNext wasn't previously set
        run(['/sbin/efibootmgr', '-n', current_boot])


# repos/system_upgrade/common/libraries/fetch.py

import os
import io  # Python2/Python3 compatible IO (open etc.)

import requests

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import get_env
from leapp.libraries.stdlib import api

SERVICE_HOST_DEFAULT = "https://cert.cloud.redhat.com"
REQUEST_TIMEOUT = (5, 30)
MAX_ATTEMPTS = 3


def _raise_error(local_path, details):
    """
    If the file acquisition fails in any way, throw an informative error to stop the actor.
    """
    summary = "Data file {lp} is invalid or could not be retrieved.".format(lp=local_path)
    hint = ("Read documentation at: https://access.redhat.com/articles/3664871"
            " for more information about how to retrieve the file.")

    raise StopActorExecutionError(summary, details={'details': details, 'hint': hint})


def _request_data(service_path, cert, proxies, timeout=REQUEST_TIMEOUT):
    logger = api.current_logger()
    attempt = 0
    while True:
        attempt += 1
        try:
            return requests.get(service_path, cert=cert, proxies=proxies, timeout=REQUEST_TIMEOUT)
        except requests.exceptions.Timeout as e:
            etype_msg = 'Connection timeout'
            if isinstance(e, requests.exceptions.ReadTimeout):
                etype_msg = 'Read timeout'
                # reading is slow, increase the time limit for the reading
                timeout = (timeout[0], timeout[1] + 10)
            if attempt > MAX_ATTEMPTS:
                logger.warning(
                    'Attempt {} of {} to get {} failed: {}.'
                    .format(MAX_ATTEMPTS, MAX_ATTEMPTS, service_path, etype_msg)
                )
                raise

            logger.info(
                'Attempt {} of {} to get {} failed: {}. Retrying...'
                .format(attempt, MAX_ATTEMPTS, service_path, etype_msg)
            )


def read_or_fetch(filename, directory="/etc/leapp/files", service=None, allow_empty=False, encoding='utf-8'):
    """
    Return the contents of a text file or fetch them from an online service if the file does not exist.

    :param str filename: The name of the file to read or fetch.
    :param str directory: Directory that should contain the file.
    :param str service: URL to the service providing the data if the file is missing.
    :param bool allow_empty: Raise an error if the resulting data are empty.
    :param str encoding: Encoding to use when decoding the raw binary data.
    :returns: Text contents of the file. Text is decoded using the provided encoding.
    :rtype: str
    """
    logger = api.current_logger()
    local_path = os.path.join(directory, filename)

    # try to get the data locally
    if not os.path.exists(local_path):
        logger.warning("File {lp} does not exist, falling back to online service".format(lp=local_path))
    else:
        try:
            with io.open(local_path, encoding=encoding) as f:
                data = f.read()
                if not allow_empty and not data:
                    _raise_error(local_path, "File {lp} exists but is empty".format(lp=local_path))
                logger.warning("File {lp} successfully read ({l} bytes)".format(lp=local_path, l=len(data)))
                return data
        except EnvironmentError:
            _raise_error(local_path, "File {lp} exists but couldn't be read".format(lp=local_path))
        except Exception as e:
            raise e

    # if the data is not present locally, fetch it from the online service
    service = service or get_env("LEAPP_SERVICE_HOST", default=SERVICE_HOST_DEFAULT)
    service_path = "{s}/api/pes/{f}".format(s=service, f=filename)
    proxy = get_env("LEAPP_PROXY_HOST")
    proxies = {"https": proxy} if proxy else None
    cert = ("/etc/pki/consumer/cert.pem", "/etc/pki/consumer/key.pem")
    response = None
    try:
        response = _request_data(service_path, cert=cert, proxies=proxies)
    except requests.exceptions.RequestException as e:
        logger.error(e)
        _raise_error(local_path, "Could not fetch {f} from {sp} (unreachable address).".format(
            f=filename, sp=service_path))
    # almost certainly missing certs
    except (OSError, IOError) as e:
        logger.error(e)
        _raise_error(local_path, ("Could not fetch {f} from {sp} (missing certificates). Is the machine"
                                  " registered?".format(f=filename, sp=service_path)))
    if response.status_code != 200:
        _raise_error(local_path, "Could not fetch {f} from {sp} (error code: {e}).".format(
            f=filename, sp=service_path, e=response.status_code))

    if not allow_empty and not response.content:
        _raise_error(local_path, "File {lp} successfully retrieved but it's empty".format(lp=local_path))
    logger.warning("File {sp} successfully retrieved and read ({l} bytes)".format(
        sp=service_path, l=len(response.content)))

    return response.content.decode(encoding)


# repos/system_upgrade/common/libraries/grub.py

import os

from leapp.exceptions import StopActorExecution
from leapp.libraries.stdlib import api, CalledProcessError, run


def has_grub(blk_dev):
    """
    Check whether GRUB is present on block device
    """
    try:
        blk = os.open(blk_dev, os.O_RDONLY)
        mbr = os.read(blk, 512)
    except OSError:
        api.current_logger().warning(
            'Could not read first sector of {} in order to identify the bootloader'.format(blk_dev)
        )
        raise StopActorExecution()
    os.close(blk)
    test = 'GRUB'
    if not isinstance(mbr, str):
        test = test.encode('utf-8')

    return test in mbr


def blk_dev_from_partition(partition):
    """
    Find parent device of /boot partition
    """
    try:
        result = run(['lsblk', '-spnlo', 'name', partition])
    except CalledProcessError:
        api.current_logger().warning(
            'Could not get parent device of {} partition'.format(partition)
        )
        raise StopActorExecution()
    # lsblk "-s" option prints dependencies in inverse order, so the parent device will always
    # be the last or the only device.
    # Command result example:
    # 'result', {'signal': 0, 'pid': 3872, 'exit_code': 0, 'stderr': u'', 'stdout': u'/dev/vda1\n/dev/vda\n'}
    return result['stdout'].strip().split()[-1]


def get_boot_partition():
    """
    Get /boot partition name
    """
    try:
        # call grub2-probe to identify /boot partition
        result = run(['grub2-probe', '--target=device', '/boot'])
    except CalledProcessError:
        api.current_logger().warning(
            'Could not get name of underlying /boot partition'
        )
        raise StopActorExecution()
    boot_partition = result['stdout'].strip()
    api.current_logger().info('/boot is on {}'.format(boot_partition))
    return boot_partition


def get_grub_device():
    """
    Get block device where GRUB is located. We assume GRUB is on the same device
    as /boot partition is.

    """
    boot_partition = get_boot_partition()
    grub_dev = blk_dev_from_partition(boot_partition)
    api.current_logger().info('GRUB is installed on {}'.format(grub_dev))
    # if has_grub(grub_dev):
    return grub_dev if has_grub(grub_dev) else None


def is_blscfg_enabled_in_defaultgrub(default_grub_msg):
    """
    Check if GRUB_ENABLE_BLSCFG is true in /etc/default/grub file
    """
    grub_options_lst = default_grub_msg.default_grub_info
    default_grub_options = {
        option.name: option.value.strip('"') for option in grub_options_lst
    }
    return bool(default_grub_options.get('GRUB_ENABLE_BLSCFG', '') == 'true')


# repos/system_upgrade/common/libraries/guards.py

import contextlib
import os

from six.moves.urllib.error import URLError
from six.moves.urllib.request import urlopen

from leapp.libraries.stdlib import CalledProcessError
from leapp.exceptions import StopActorExecutionError


@contextlib.contextmanager
def guarded_execution(*guards):
    try:
        yield
    except CalledProcessError as e:
        # collect output from guards for possible spurious failure
        guard_errors = []
        for guard in guards:
            err = guard()
            if err:
                guard_errors.append(err)

        details = None
        if guard_errors:
            details = {
                'hint': 'Possible spurious failure: {cause}'.format(cause=' '.join(guard_errors))
            }
        raise StopActorExecutionError(
            message=str(e),
            details=details
        )


def connection_guard(url='https://example.com'):
    def closure():
        try:
            urlopen(url)
            return None
        except URLError as e:
            cause = '''Failed to open url '{url}' with error: {error}'''.format(url=url, error=e)
            return ('There was probably a problem with internet conection ({cause}).'
                    ' Check your connection and try again.'.format(cause=cause))
    return closure


def space_guard(path='/', min_free_mb=100):
    def closure():
        info = os.statvfs(path)
        free_mb = (info.f_bavail * info.f_frsize) >> 20
        if free_mb >= min_free_mb:
            return None
        return ('''Not enough free disk space in '{path}', needed: {min} M, available: {avail} M.'''
                ' Free more disk space and try again.'.format(path=path, min=min_free_mb, avail=free_mb))
    return closure


# repos/system_upgrade/common/libraries/module.py

import warnings

try:
    import dnf
except ImportError:
    dnf = None
    warnings.warn('Could not import the `dnf` python module.', ImportWarning)

try:
    import hawkey
except ImportError:
    hawkey = None
    warnings.warn('Could not import the `hawkey` python module.', ImportWarning)


def _create_or_get_dnf_base(base=None):
    if not base:
        base = dnf.Base()
        base.init_plugins()
        base.read_all_repos()
        # configure plugins after the repositories are loaded
        # e.g. the amazon-id plugin requires loaded repositories
        # for the proper configuration.
        base.configure_plugins()
        base.fill_sack()
    return base


def get_modules(base=None):
    """
    Return info about all module streams as a list of libdnf.module.ModulePackage objects.
    """
    if not dnf:
        return []
    base = _create_or_get_dnf_base(base)

    module_base = dnf.module.module_base.ModuleBase(base)
    # this method is absent on RHEL 7, in which case there are no modules anyway
    if not hasattr(module_base, 'get_modules'):
        return []
    return module_base.get_modules('*')[0]


def get_enabled_modules():
    """
    Return currently enabled module streams as a list of libdnf.module.ModulePackage objects.
    """
    if not dnf:
        return []

    base = _create_or_get_dnf_base()
    modules = get_modules(base)

    # if modules are not supported (RHEL 7), base.sack._moduleContainer won't exist
    # luckily in that case modules are empty and the element won't even be accessed
    return [m for m in modules if base.sack._moduleContainer.isEnabled(m)]


def map_installed_rpms_to_modules():
    """
    Map installed modular packages to the module streams they come from.
    """
    modules = get_modules()
    # empty on RHEL 7 because of no modules
    if not modules:
        return {}
    # create a reverse mapping from the RPMS to module streams
    # key: tuple of 4 strings representing a NVRA (name, version, release, arch) of an RPM
    # value: tuple of 2 strings representing a module and its stream
    rpm_streams = {}
    for module in modules:
        for rpm in module.getArtifacts():
            nevra = hawkey.split_nevra(rpm)
            rpm_key = (nevra.name, nevra.version, nevra.release, nevra.arch)
            rpm_streams[rpm_key] = (module.getName(), module.getStream())
    return rpm_streams


# repos/system_upgrade/common/libraries/mounting.py

import errno
import itertools
import os
import shutil
from collections import namedtuple

from leapp.libraries.stdlib import run, CalledProcessError, api
from leapp.libraries.common.config import get_all_envs
from leapp.libraries.common.config.version import get_source_major_version


# Using ALWAYS_BIND will crash the upgrade process if the file does not exist.
# Consider instead adding an entry to the ScanFilesToCopyIntoTargetSystem actor that
# conditionaly (only if it exists) creates CopyFile message to the TargetUserspaceCreator.
ALWAYS_BIND = []

ErrorData = namedtuple('ErrorData', ['summary', 'details'])


class MountingMode(object):
    """
    MountingMode are types of mounts supported by the library
    """
    BIND = 'bind'
    """ Used for bind mounts """
    OVERLAY = 'overlay'
    """ Used for overlayfs mounts """
    LOOP = 'loop'
    """ Used for loop mounts """
    FSTYPE = 'fstype'
    """ Used to mount specific filesystem types such as procfs, sysfs etc """
    NONE = 'none'
    """ Used when no actual mount call needs to be issued """


def _makedirs(path, mode=0o777, exists_ok=True):
    """ Helper function which extends os.makedirs with exists_ok on all versions of python. """
    try:
        os.makedirs(path, mode=mode)
    except OSError:
        if not exists_ok or not os.path.isdir(path):
            raise


class MountError(Exception):
    """ Exception that is thrown when a mount related operation failed """

    def __init__(self, message, details):
        super(MountError, self).__init__(message)
        self.details = details


class IsolationType(object):
    """ Implementations for the different isolated actions types """
    class _Implementation(object):
        """ Base class for all isolated actions """

        def __init__(self, target, **kwargs):
            self.target = target

        def create(self):
            """ Create the isolation context """
            pass

        def close(self):
            """ Release the isolation context """
            pass

        def make_command(self, cmd):
            """ Transform the given command to the isolated environment """
            return cmd

    class NSPAWN(_Implementation):
        """ systemd-nspawn implementation """

        def __init__(self, target, binds=(), env_vars=None):
            super(IsolationType.NSPAWN, self).__init__(target=target)
            self.binds = list(binds) + ALWAYS_BIND
            self.env_vars = env_vars or get_all_envs()

        def make_command(self, cmd):
            """ Transform the command to be executed with systemd-nspawn """
            binds = ['--bind={}'.format(bind) for bind in self.binds]
            setenvs = ['--setenv={}={}'.format(env.name, env.value) for env in self.env_vars]
            final_cmd = ['systemd-nspawn', '--register=no', '--quiet']
            if get_source_major_version() != '7':
                # TODO: check whether we could use the --keep unit on el7 too.
                # in such a case, just add line into the previous solution..
                # TODO: the same about --capability=all
                final_cmd += ['--keep-unit', '--capability=all']
            return final_cmd + ['-D', self.target] + binds + setenvs + cmd

    class CHROOT(_Implementation):
        """ chroot implementation """

        def __init__(self, target):
            super(IsolationType.CHROOT, self).__init__(target)
            self.context = None

        def create(self):
            """ Create the necessary context for chroot based isolation """
            self.close()
            self.context = self._create_context()
            next(self.context)

        def _create_context(self):
            """ This will mount /proc, /sys and /dev for chroot executions """
            with TypedMount('proc', 'proc', os.path.join(self.target, 'proc')):
                with TypedMount('sysfs', 'sys', os.path.join(self.target, 'sys')):
                    with BindMount('/dev', os.path.join(self.target, 'dev')):
                        yield

        def close(self):
            """ Releasing the context and perform unmounting """
            if self.context:
                next(self.context)
            self.context = None

        def make_command(self, cmd):
            """ Transform the command to be executed in the chrooted environment """
            return [
                'chroot', self.target
            ] + cmd

    class NONE(_Implementation):
        """ Execute the given commands and perform the given operations on the real system and not isolated. """


class IsolatedActions(object):
    """ This class allows to perform actions in a manner as if the given base_dir would be the current root """

    _isolated = True

    def __init__(self, base_dir, implementation, **kwargs):
        self.base_dir = base_dir
        self.type = implementation(base_dir, **kwargs)

    def __enter__(self):
        self.type.create()
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        self.type.close()

    def full_path(self, path):
        """
        Transform the path given to be prefixed with the base_dir, to get the real path on the system.

        The function is secured, so it is not possible to return path outside
        of the self.base_dir directory.

        Example: self.base_dir = '/var/lib/leapp/scratch/userspace'
                 path = '/etc/yum.repos.d/redhat.repo'
                 The result would be: /var/lib/leapp/scratch/userspace/etc/yum.repos.d/redhat.repo
        """
        return os.path.join(self.base_dir, os.path.abspath(path).lstrip('/'))

    def open(self, path, *args, **kwargs):
        """
        Open the path given as if it would be the real system.

        The only difference between this function and the python builtin open is the fact that the path uses
        self.full_path to translate the passed path argument. All other arguments are passed through
        """
        return open(self.full_path(path), *args, **kwargs)

    def call(self, cmd, *args, **kwargs):
        """ Running the given command using the leapp.libraries.stdlib.run function in a isolated manner. """
        return run(self.type.make_command(cmd), *args, **kwargs)

    def remove(self, path):
        """
        Removes the given file as it would be on the real system.
        """
        os.unlink(self.full_path(path))

    def remove_tree(self, path):
        """
        Removes the given directory recursively inside the isolated environment
        as it would be on the real system.

        If the destination doesn't exist, nothing happens.
        """
        try:
            shutil.rmtree(self.full_path(path))
        except EnvironmentError as e:
            # this is recommended way to handle it in Py2 & Py3
            if e.errno != errno.ENOENT:
                raise

    def mkdir(self, path, mode=0o777):
        """
        Creates the given path as it would be on the real system.
        """
        os.mkdir(self.full_path(path), mode=mode)

    def makedirs(self, path, mode=0o777, exists_ok=True):
        """
        Creates the whole path recursively for any missing part.
        """
        _makedirs(path=self.full_path(path), mode=mode, exists_ok=exists_ok)

    def copytree_to(self, src, dst):
        """
        Recursively copy an entire directory tree rooted at src. The destination directory,
        named by dst, must not already exist; it will be created as well as missing parent directories.

        The destination directory is considered to be in the isolated environment.
        The source directory is considered to be on the current system root.
        """
        shutil.copytree(src, self.full_path(dst))

    def copytree_from(self, src, dst):
        """
        Recursively copy an entire directory tree rooted at src. The destination directory,
        named by dst, must not already exist; it will be created as well as missing parent directories.

        The destination directory is considered to be on the current system root.
        The source directory is considered to be in the isolated environment.
        """
        shutil.copytree(self.full_path(src), dst)

    def copy_to(self, src, dst):
        """
        Copy the file src to the file or directory dst. If dst is a directory, a file with the same basename
        as src is created (or overwritten) in the directory specified. Permission bits are copied. src and dst
        are path names given as strings.
        copy_to also attempts to preserve file metadata.

        The source is expected to be on the current system.
        The destination is expected to be in the isolated environment.
        """
        shutil.copy2(src, self.full_path(dst))

    def copy_from(self, src, dst):
        """
        Copy the file src to the file or directory dst. If dst is a directory, a file with the same basename
        as src is created (or overwritten) in the directory specified. Permission bits are copied. src and dst
        are path names given as strings.
        copy_to also attempts to preserve file metadata.

        The source is expected to be in the isolated environment.
        The destination is expected to be on the current system.
        """
        shutil.copy2(self.full_path(src), dst)

    @classmethod
    def is_isolated(cls):
        """
        Tell whether the context is isolated or not.

        All classes except NotIsolatedActions return True.
        """
        return cls._isolated


class ChrootActions(IsolatedActions):
    """ Isolation with chroot """

    def __init__(self, base_dir):
        super(ChrootActions, self).__init__(base_dir=base_dir, implementation=IsolationType.CHROOT)


class NspawnActions(IsolatedActions):
    """ Isolation with systemd-nspawn """

    def __init__(self, base_dir, binds=(), env_vars=None):
        super(NspawnActions, self).__init__(
            base_dir=base_dir, implementation=IsolationType.NSPAWN, binds=binds, env_vars=env_vars)


class NotIsolatedActions(IsolatedActions):
    """ Non isolated executed. """
    _isolated = False

    def __init__(self, base_dir):
        super(NotIsolatedActions, self).__init__(base_dir=base_dir, implementation=IsolationType.NONE)


class MountConfig(object):
    """ Options for Mount """
    _Options = namedtuple('_Options', ('should_create', 'should_cleanup'))
    AttachOnly = _Options(should_create=False, should_cleanup=False)
    """ Do not perform any mount operations, and do not clean up afterwards """
    Attach = _Options(should_create=False, should_cleanup=True)
    """ Do not perform any mount operations, however cleanup afterwards """
    MountOnly = _Options(should_create=True, should_cleanup=False)
    """ Create all necessary directories and perform mount calls, but do not cleanup afterwards """
    Mount = _Options(should_create=True, should_cleanup=True)
    """ Create all necessary directories and perform mount calls and cleanup afterwards """


class MountingBase(object):
    """ Base class for all mount operations """

    def __init__(self, source, target, mode, config=MountConfig.Mount):
        self._mode = mode
        self.source = source
        self.target = target
        self._config = config
        self.additional_directories = ()

    def _mount_options(self):
        """
        Options to use with the mount call, individual implementations may override this function to return the
        correct parameters
        """
        return ['-o', self._mode, self.source]

    def chroot(self):
        """ Create a ChrootActions instance for this mount """
        return ChrootActions(self.target)

    def nspawn(self):
        """ Create a NspawnActions instance for this mount """
        return NspawnActions(self.target)

    def real(self):
        """ Create a NotIsolatedActions instance for this mount """
        return NotIsolatedActions(self.target)

    def _cleanup(self):
        """ Cleanup operations """
        if os.path.exists(self.target) and os.path.ismount(self.target):
            try:
                run(['umount', '-fl', self.target], split=False)
            except (OSError, CalledProcessError) as e:
                api.current_logger().warning('Unmounting %s failed with: %s', self.target, str(e))
        for directory in itertools.chain(self.additional_directories, (self.target,)):
            try:
                run(['rm', '-rf', directory], split=False)
            except (OSError, CalledProcessError) as e:
                api.current_logger().warning('Removing mount directory %s failed with: %s', directory, str(e))

    def mount(self):
        """ Performs the mount if MountConfig.should_create = True """
        if self._config.should_create:
            self._create()

    def _create(self):
        self._cleanup()
        for directory in itertools.chain(self.additional_directories, (self.target,)):
            try:
                _makedirs(directory, exists_ok=True)
            except (OSError) as e:
                raise MountError('Failed to create mount target directory {}'.format(directory), str(e))
        try:
            run(['mount'] + self._mount_options() + [self.target], split=False)
        except (OSError, CalledProcessError) as e:
            api.current_logger().warning('Mounting %s failed with: %s', self.target, str(e), exc_info=True)
            raise MountError(
                message='Mount operation with mode {} from {} to {} failed: {}'.format(
                    self._mode, self.source, self.target, str(e)),
                details=None)
        return self

    def umount(self):
        """ Performs the umount if MountConfig.should_cleanup = True """
        if self._config.should_cleanup:
            self._cleanup()

    def __enter__(self):
        self.mount()
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        self.umount()


class NullMount(MountingBase):
    """ This is basically a NoOp for compatibility with other mount operations, in case a mount is optional """

    def __init__(self, target, config=MountConfig.AttachOnly):
        super(NullMount, self).__init__(source=target, target=target, mode=MountingMode.NONE, config=config)

    def __enter__(self):
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        pass


class LoopMount(MountingBase):
    """ Performs loop mounts """

    def __init__(self, source, target, config=MountConfig.Mount):
        super(LoopMount, self).__init__(source=source, target=target, mode=MountingMode.LOOP, config=config)


class BindMount(MountingBase):
    """ Performs bind mounts """

    def __init__(self, source, target, config=MountConfig.Mount):
        super(BindMount, self).__init__(source=source, target=target, mode=MountingMode.BIND, config=config)


class TypedMount(MountingBase):
    """ Performs a typed mounts """

    def __init__(self, fstype, source, target, config=MountConfig.Mount):
        super(TypedMount, self).__init__(source=source, target=target, mode=MountingMode.FSTYPE, config=config)
        self.fstype = fstype

    def _mount_options(self):
        return [
            '-t', self.fstype,
            self.source
        ]


class OverlayMount(MountingBase):
    """ Performs an overlayfs mount """

    def __init__(self, name, source, workdir, config=MountConfig.Mount):
        super(OverlayMount, self).__init__(source=source, target=os.path.join(workdir, name),
                                           mode=MountingMode.OVERLAY, config=config)
        self._upper_dir = os.path.join(workdir, 'upper')
        self._work_dir = os.path.join(workdir, 'work')
        self.additional_directories = (self._upper_dir, self._work_dir)

    def _mount_options(self):
        return [
            '-t', 'overlay', 'overlay2',
            '-o', 'lowerdir={},upperdir={},workdir={}'.format(self.source, self._upper_dir, self._work_dir)
        ]


# repos/system_upgrade/common/libraries/multipathutil.py

import errno
import re

from leapp.libraries.stdlib import api

_sections = ('defaults', 'blacklist', 'blacklist_exceptions', 'devices',
             'overrides', 'multipaths')

_subsections = {'blacklist': 'device', 'blacklist_exceptions': 'device',
                'devices': 'device', 'multipaths': 'multipath'}


def read_config(path):
    try:
        with open(path, 'r') as f:
            return f.read()
    except IOError as e:
        if e.errno == errno.ENOENT:
            api.current_logger().debug(
                'multipath configuration file {} does not exist.'.format(path)
            )
        else:
            api.current_logger().warning(
                'Failed to read multipath configuration file {}: {}'.
                format(path, e)
            )
        return None


def write_config(path, contents):
    try:
        with open(path, 'w') as f:
            f.write(contents)
    except IOError as e:
        api.current_logger().warning(
            'Failed to write multipath configuration file {}: {}'.
            format(path, e)
        )


class LineData(object):
    TYPE_BLANK = 0
    TYPE_SECTION_START = 1
    TYPE_SECTION_END = 2
    TYPE_OPTION = 3

    def __init__(self, line, section, in_subsection):
        comment_pattern = re.compile('^([^"#!]*)[#!]')
        string_pattern = re.compile('^([^"]*)"([^"]*)')
        utf8_pattern = re.compile('[^\x00-\x7F]+')
        line_pattern = re.compile(r'^([^\s{}]+)\s*(\S*)')
        value = None

        r = comment_pattern.match(line)
        if r:
            line = r.group(1)
        r = string_pattern.match(line)
        if r:
            line = r.group(1)
            value = r.group(2)
        line = utf8_pattern.sub(' ', line)
        line = line.strip()

        if line == '':
            self.type = self.TYPE_BLANK
            return
        if line[0] == '}':
            self.type = self.TYPE_SECTION_END
            return

        r = line_pattern.match(line)
        if r is None:
            raise ValueError
        keyword = r.group(1)
        if r.group(2) != '':
            value = r.group(2)  # even if value was set before

        if section is None:
            if keyword in _sections:
                self.type = self.TYPE_SECTION_START
                self.section = keyword
                return
            raise ValueError

        if not in_subsection and section in _subsections and \
                keyword == _subsections[section]:
            self.type = self.TYPE_SECTION_START
            self.section = keyword
            return

        if value is None:
            raise ValueError
        self.type = self.TYPE_OPTION
        self.option = keyword
        self.value = value

    def is_enabled(self):
        if self.value in ('yes', '1'):
            return True
        if self.value in ('no', '0'):
            return False
        return None


# repos/system_upgrade/common/libraries/overlaygen.py

import contextlib
import os
import shutil
from collections import namedtuple

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import mounting, utils
from leapp.libraries.stdlib import CalledProcessError, api, run


OVERLAY_DO_NOT_MOUNT = ('tmpfs', 'devpts', 'sysfs', 'proc', 'cramfs', 'sysv', 'vfat')


MountPoints = namedtuple('MountPoints', ['fs_file', 'fs_vfstype'])


def _ensure_enough_diskimage_space(space_needed, directory):
    stat = os.statvfs(directory)
    if (stat.f_frsize * stat.f_bavail) < (space_needed * 1024 * 1024):
        message = ('Not enough space available for creating required disk images in {directory}. ' +
                   'Needed: {space_needed} MiB').format(space_needed=space_needed, directory=directory)
        api.current_logger().error(message)
        raise StopActorExecutionError(message)


def _get_mountpoints(storage_info):
    mount_points = set()
    for entry in storage_info.fstab:
        if os.path.isdir(entry.fs_file) and entry.fs_vfstype not in OVERLAY_DO_NOT_MOUNT:
            mount_points.add(MountPoints(entry.fs_file, entry.fs_vfstype))
        elif os.path.isdir(entry.fs_file) and entry.fs_vfstype == 'vfat':
            api.current_logger().warning(
                'Ignoring vfat {} filesystem mount during upgrade process'.format(entry.fs_file)
            )

    return list(mount_points)


def _mount_name(mountpoint):
    return 'root{}'.format(mountpoint.replace('/', '_'))


def _mount_dir(mounts_dir, mountpoint):
    return os.path.join(mounts_dir, _mount_name(mountpoint))


def _prepare_required_mounts(scratch_dir, mounts_dir, mount_points, xfs_info):
    result = {
        mount_point.fs_file: mounting.NullMount(
            _mount_dir(mounts_dir, mount_point.fs_file)) for mount_point in mount_points
    }

    if not xfs_info.mountpoints_without_ftype:
        return result

    space_needed = _overlay_disk_size() * len(xfs_info.mountpoints_without_ftype)
    disk_images_directory = os.path.join(scratch_dir, 'diskimages')

    # Ensure we cleanup old disk images before we check for space contraints.
    run(['rm', '-rf', disk_images_directory])
    _create_diskimages_dir(scratch_dir, disk_images_directory)
    _ensure_enough_diskimage_space(space_needed, scratch_dir)

    mount_names = [mount_point.fs_file for mount_point in mount_points]

    # TODO(pstodulk): this (adding rootfs into the set always) is hotfix for
    # bz #1911802 (not ideal one..). The problem occurs one rootfs is ext4 fs,
    # but /var/lib/leapp/... is under XFS without ftype; In such a case we can
    # see still the very same problems as before. But letting you know that
    # probably this is not the final solution, as we could possibly see the
    # same problems on another partitions too (needs to be tested...). However,
    # it could fit for now until we provide the complete solution around XFS
    # workarounds (including management of required spaces for virtual FSs per
    # mountpoints - without that, we cannot fix this properly)
    for mountpoint in set(xfs_info.mountpoints_without_ftype + ['/']):
        if mountpoint in mount_names:
            image = _create_mount_disk_image(disk_images_directory, mountpoint)
            result[mountpoint] = mounting.LoopMount(source=image, target=_mount_dir(mounts_dir, mountpoint))
    return result


@contextlib.contextmanager
def _build_overlay_mount(root_mount, mounts):
    if not root_mount:
        raise StopActorExecutionError('Root mount point has not been prepared for overlayfs.')
    if not mounts:
        yield root_mount
    else:
        current = list(mounts.keys())[0]
        current_mount = mounts.pop(current)
        name = _mount_name(current)
        with current_mount:
            with mounting.OverlayMount(name=name, source=current, workdir=current_mount.target) as overlay:
                with mounting.BindMount(source=overlay.target,
                                        target=os.path.join(root_mount.target, current.lstrip('/'))):
                    with _build_overlay_mount(root_mount, mounts) as mount:
                        yield mount


def _overlay_disk_size():
    """
    Convenient function to retrieve the overlay disk size
    """
    try:
        env_size = os.getenv('LEAPP_OVL_SIZE', default='2048')
        disk_size = int(env_size)
    except ValueError:
        disk_size = 2048
        api.current_logger().warning(
            'Invalid "LEAPP_OVL_SIZE" environment variable "%s". Setting default "%d" value', env_size, disk_size
        )
    return disk_size


def cleanup_scratch(scratch_dir, mounts_dir):
    """
    Function to cleanup the scratch directory
    """
    api.current_logger().debug('Cleaning up mounts')
    if os.path.ismount(mounts_dir):
        api.current_logger().debug('Mounts directory is a mounted disk image - Unmounting.')
        try:
            run(['/bin/umount', '-fl', mounts_dir])
            api.current_logger().debug('Unmounted mounted disk image.')
        except (OSError, CalledProcessError) as e:
            api.current_logger().warning('Failed to umount %s - message: %s', mounts_dir, str(e))
    api.current_logger().debug('Recursively removing scratch directory %s.', scratch_dir)
    shutil.rmtree(scratch_dir, onerror=utils.report_and_ignore_shutil_rmtree_error)
    api.current_logger().debug('Recursively removed scratch directory %s.', scratch_dir)


def _create_mount_disk_image(disk_images_directory, path):
    """
    Creates the mount disk image, for cases when we hit XFS with ftype=0
    """
    diskimage_path = os.path.join(disk_images_directory, _mount_name(path))
    disk_size = _overlay_disk_size()

    api.current_logger().debug('Attempting to create disk image with size %d MiB at %s', disk_size, diskimage_path)
    utils.call_with_failure_hint(
        cmd=['/bin/dd', 'if=/dev/zero', 'of={}'.format(diskimage_path), 'bs=1M', 'count={}'.format(disk_size)],
        hint='Please ensure that there is enough diskspace in {} at least {} MiB are needed'.format(
            diskimage_path, disk_size)
    )

    api.current_logger().debug('Creating ext4 filesystem in disk image at %s', diskimage_path)
    try:
        utils.call_with_oserror_handled(cmd=['/sbin/mkfs.ext4', '-F', diskimage_path])
    except CalledProcessError as e:
        api.current_logger().error('Failed to create ext4 filesystem %s', exc_info=True)
        raise StopActorExecutionError(
            message=str(e)
        )

    return diskimage_path


def _create_diskimages_dir(scratch_dir, diskimages_dir):
    """
    Prepares directories for disk images
    """
    api.current_logger().debug('Creating disk images directory.')
    try:
        utils.makedirs(diskimages_dir)
        api.current_logger().debug('Done creating disk images directory.')
    except OSError:
        api.current_logger().error('Failed to create disk images directory %s', diskimages_dir, exc_info=True)

        # This is an attempt for giving the user a chance to resolve it on their own
        raise StopActorExecutionError(
            message='Failed to prepare environment for package download while creating directories.',
            details={
                'hint': 'Please ensure that {scratch_dir} is empty and modifiable.'.format(scratch_dir=scratch_dir)
            }
        )


def _create_mounts_dir(scratch_dir, mounts_dir):
    """
    Prepares directories for mounts
    """
    api.current_logger().debug('Creating mount directories.')
    try:
        utils.makedirs(mounts_dir)
        api.current_logger().debug('Done creating mount directories.')
    except OSError:
        api.current_logger().error('Failed to create mounting directories %s', mounts_dir, exc_info=True)

        # This is an attempt for giving the user a chance to resolve it on their own
        raise StopActorExecutionError(
            message='Failed to prepare environment for package download while creating directories.',
            details={
                'hint': 'Please ensure that {scratch_dir} is empty and modifiable.'.format(scratch_dir=scratch_dir)
            }
        )


@contextlib.contextmanager
def _mount_dnf_cache(overlay_target):
    """
    Convenience context manager to ensure bind mounted /var/cache/dnf and removal of the mount.
    """
    with mounting.BindMount(
            source='/var/cache/dnf',
            target=os.path.join(overlay_target, 'var', 'cache', 'dnf')) as cache_mount:
        yield cache_mount


@contextlib.contextmanager
def create_source_overlay(mounts_dir, scratch_dir, xfs_info, storage_info, mount_target=None):
    """
    Context manager that prepares the source system overlay and yields the mount.
    """
    api.current_logger().debug('Creating source overlay in {scratch_dir} with mounts in {mounts_dir}'.format(
        scratch_dir=scratch_dir, mounts_dir=mounts_dir))
    try:
        _create_mounts_dir(scratch_dir, mounts_dir)
        mounts = _prepare_required_mounts(scratch_dir, mounts_dir, _get_mountpoints(storage_info), xfs_info)
        with mounts.pop('/') as root_mount:
            with mounting.OverlayMount(name='system_overlay', source='/', workdir=root_mount.target) as root_overlay:
                if mount_target:
                    target = mounting.BindMount(source=root_overlay.target, target=mount_target)
                else:
                    target = mounting.NullMount(target=root_overlay.target)
                with target:
                    with _build_overlay_mount(root_overlay, mounts) as overlay:
                        with _mount_dnf_cache(overlay.target):
                            yield overlay
    except Exception:
        cleanup_scratch(scratch_dir, mounts_dir)
        raise


# repos/system_upgrade/common/libraries/persistentnetnames.py

import pyudev

from leapp.libraries.stdlib import api
from leapp.models import Interface, PCIAddress

udev_context = pyudev.Context()


def physical_interfaces():
    """
    Returns a list of pyudev.Device objects for all physical network interfaces
    """
    enumerator = pyudev.Enumerator(udev_context).match_subsystem('net')
    return [d for d in enumerator if not d.device_path.startswith('/devices/virtual/')]


def pci_info(path):
    """
    Returns PCI topology info from string which is expected to be a value of ID_PATH udev device property
    """
    pci = {}

    # TODO(msekleta): check that path argument actually has ID_PATH format
    if path.startswith('pci-'):
        components = path[4:16].split(':')
        pci['domain'] = components[0]
        pci['bus'] = components[1]
        pci['device'] = components[2].split('.')[0]
        pci['function'] = components[2].split('.')[1]

    return pci


def interfaces():
    """
    Generator which produces an Interface objects containing assorted interface properties relevant for network naming
    """
    for dev in physical_interfaces():
        attrs = {}

        try:
            attrs['name'] = dev.sys_name
            attrs['devpath'] = dev.device_path
            attrs['driver'] = dev['ID_NET_DRIVER']
            attrs['vendor'] = dev['ID_VENDOR_ID']
            attrs['pci_info'] = PCIAddress(**pci_info(dev['ID_PATH']))
            attrs['mac'] = dev.attributes.get('address')
            if isinstance(attrs['mac'], bytes):
                attrs['mac'] = attrs['mac'].decode()
        except Exception as e:  # pylint: disable=broad-except
            # FIXME(msekleta): We should probably handle errors more granularly
            # Maybe we should inhibit upgrade process at this point
            api.current_logger().warning('Failed to gather information about network interface: ' + str(e))
            continue

        yield Interface(**attrs)


# repos/system_upgrade/common/libraries/repofileutils.py

import json
import os

from leapp.libraries.common import mounting, utils
from leapp.libraries.stdlib import api
from leapp.models import RepositoryFile, RepositoryData, fields

try:
    import dnf
except ImportError:
    api.current_logger().warning('repofileutils.py: failed to import dnf')


def _parse_repository(repoid, repo_data):
    def asbool(x):
        return x == '1'
    prepared = {'repoid': repoid, 'additional_fields': {}}
    for key in repo_data.keys():
        if key in RepositoryData.fields:
            if isinstance(RepositoryData.fields[key], fields.Boolean):
                repo_data[key] = asbool(repo_data[key])
            prepared[key] = repo_data[key]
        else:
            prepared['additional_fields'][key] = repo_data[key]
    prepared['additional_fields'] = json.dumps(prepared['additional_fields'])
    return RepositoryData(**prepared)


def parse_repofile(repofile):
    """
    Parse the given repo file.

    :param repofile: Path to the repo file
    :type repofile: str
    :rtype: RepositoryFile
    """
    data = []
    with open(repofile, mode='r') as fp:
        cp = utils.parse_config(fp, strict=False)
        for repoid in cp.sections():
            data.append(_parse_repository(repoid, dict(cp.items(repoid))))
    return RepositoryFile(file=repofile, data=data)


def get_repodirs():
    """
    Return all directories yum scans for repository files, if they exist.
    By default, the possible paths on RHEL should be:
    ['/etc/yum.repos.d', '/etc/yum/repos.d', '/etc/distro.repos.d']

    ATTENTION: Requires the dnf module to be present.
    TODO: Get repodirs inside given context.
    """
    with dnf.base.Base() as base:
        base.conf.read(priority=dnf.conf.PRIO_MAINCONFIG)
        return list({os.path.realpath(d) for d in base.conf.reposdir if os.path.isdir(d)})


def get_parsed_repofiles(context=mounting.NotIsolatedActions(base_dir='/')):
    """
    Scan all repositories on the system.

    Repositories are scanned under repository directories (as reported by dnf)
    of the given context. By default the context is the host system.

    ATTENTION: Do not forget to ensure the redhat.repo file is regenerated
    by RHSM when used.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :rtype: List(RepositoryFile)
    """
    repofiles = []
    cmd = ['find', '-L'] + get_repodirs() + ['-maxdepth', '1', '-type', 'f', '-name', '*.repo']
    repofiles_paths = context.call(cmd, split=True)['stdout']
    for repofile_path in repofiles_paths:
        repofile = parse_repofile(context.full_path(repofile_path))
        # we want full path in cotext, not the real full path
        repofile.file = repofile_path
        repofiles.append(repofile)
    return repofiles


def _invert_dict(data):
    """{a: [b]} -> {b: [a]}"""
    inv_dict = {}
    for key in data.keys():
        for value in data[key]:
            inv_dict[value] = inv_dict.get(value, []) + [key]
    return inv_dict


def get_duplicate_repositories(repofiles):
    """
    Return dict of duplicate repositories {repoid: [repofile_path]}

    A repository is defined multiple times if it exists in multiple repofiles.
    Redefinition inside one repository file is ignored (same in DNF).

    :param repofiles:
    :type repofiles: List(RepositoryFile)
    :rtype: dict {repoid: repofilepath}
    """
    rf_repos = {repofile.file: [repo.repoid for repo in repofile.data] for repofile in repofiles}
    repos = _invert_dict(rf_repos)
    return {repo: set(rfiles) for repo, rfiles in repos.items() if len(set(rfiles)) > 1}


# repos/system_upgrade/common/libraries/rhsm.py

import contextlib
import functools
import os
import re
import time

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import repofileutils
from leapp.libraries.common.config import get_env
from leapp.libraries.stdlib import CalledProcessError, api
from leapp.models import RHSMInfo

_RE_REPO_UID = re.compile(r'Repo ID:\s*([^\s]+)')
_RE_RELEASE = re.compile(r'Release:\s*([^\s]+)')
_RE_SKU_CONSUMED = re.compile(r'SKU:\s*([^\s]+)')
_ATTEMPTS = 5
_RETRY_SLEEP = 5
_DEFAULT_RHSM_REPOFILE = '/etc/yum.repos.d/redhat.repo'

SCA_TEXT = "Content Access Mode is set to Simple Content Access"


def _rhsm_retry(max_attempts, sleep=None):
    """
    A decorator to retry executing a function/method if unsuccessful.

    The function/method execution is considered unsuccessful when it raises StopActorExecutionError.

    :param max_attempts: Maximum number of attempts to execute the decorated function/method.
    :param sleep: Time to wait between attempts. In seconds.
    """
    def impl(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            attempts = 0
            while True:
                attempts += 1
                try:
                    return f(*args, **kwargs)
                except StopActorExecutionError:
                    if max_attempts <= attempts:
                        api.current_logger().warning(
                            'Attempt %d of %d to perform %s failed. Maximum number of retries has been reached.',
                            attempts, max_attempts, f.__name__)
                        raise
                    if sleep:
                        api.current_logger().info(
                            'Attempt %d of %d to perform %s failed - Retrying after %s seconds',
                            attempts, max_attempts, f.__name__, str(sleep))
                        time.sleep(sleep)
                    else:
                        api.current_logger().info(
                            'Attempt %d of %d to perform %s failed - Retrying...', attempts, max_attempts, f.__name__)
        return wrapper
    return impl


@contextlib.contextmanager
def _handle_rhsm_exceptions(hint=None):
    """
    Context manager based function that handles exceptions of `run` for the subscription-manager calls.
    """
    try:
        yield
    except OSError as e:
        api.current_logger().error('Failed to execute subscription-manager executable')
        raise StopActorExecutionError(
            message='Unable to execute subscription-manager executable: {}'.format(str(e)),
            details={
                'hint': 'Please ensure subscription-manager is installed and executable.'
            }
        )
    except CalledProcessError as e:
        _def_hint = (
            'Please ensure you have a valid RHEL subscription and your network is up.'
            ' If you are using proxy for Red Hat subscription-manager, please make sure'
            ' it is specified inside the /etc/rhsm/rhsm.conf file.'
            ' Or use the --no-rhsm option when running leapp, if you do not want to'
            ' use subscription-manager for the in-place upgrade and you want to'
            ' deliver all target repositories by yourself or using RHUI on public cloud.'
        )
        raise StopActorExecutionError(
            message='A subscription-manager command failed to execute',
            details={
                'details': str(e),
                'stderr': e.stderr,
                'hint': hint or _def_hint
            }
        )


def skip_rhsm():
    """Check whether we should skip RHSM related code."""
    return get_env('LEAPP_NO_RHSM', '0') == '1'


def with_rhsm(f):
    """Decorator to allow skipping RHSM functions by executing a no-op."""
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        if not skip_rhsm():
            return f(*args, **kwargs)
        return None
    return wrapper


@with_rhsm
def get_attached_skus(context):
    """
    Retrieve the list of the SKUs the system is attached to with the subscription-manager.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: SKUs the current system is attached to.
    :rtype: List(string)
    """
    with _handle_rhsm_exceptions():
        result = context.call(['subscription-manager', 'list', '--consumed'], split=False)
        return _RE_SKU_CONSUMED.findall(result['stdout'])


@with_rhsm
def get_rhsm_status(context):
    """
    Retrieve "subscription-manager status" output.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: "subscription manager status" output
    :rtype: String
    """
    with _handle_rhsm_exceptions():
        result = context.call(['subscription-manager', 'status'])
        return result['stdout']


def is_manifest_sca(context):
    """
    Check if SCA manifest is used in Satellite.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: True if SCA manifest is used else False
    :rtype: Boolean
    """
    return SCA_TEXT in get_rhsm_status(context)


def get_available_repo_ids(context):
    """
    Retrieve repo ids of all the repositories available through the subscription-manager.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: Repositories that are available to the current system through the subscription-manager
    :rtype: List(string)
    """
    # Regenerated redhat.repo file ...
    # FIXME: try to come up with something less invasive than yum clean all
    # but still safe to call..
    cmd = ['yum', 'clean', 'all']
    try:
        context.call(cmd)
    except CalledProcessError as exc:
        raise StopActorExecutionError(
            'Unable to use yum successfully',
            details={'details': str(exc), 'stderr': exc.stderr}
        )

    repofiles = repofileutils.get_parsed_repofiles(context)

    # TODO: move this functionality out! Create check actor that will do
    # the inhibit. The functionality is really not good here in the current
    # shape of the leapp-repository. See the targetuserspacecreator and
    # systemfacts actor if this is moved out.
    # Issue: #486
    _inhibit_on_duplicate_repos(repofiles)
    rhsm_repos = []
    for rfile in repofiles:
        if rfile.file == _DEFAULT_RHSM_REPOFILE and rfile.data:
            rhsm_repos = [repo.repoid for repo in rfile.data]
            rhsm_repos.sort()
            break

    list_separator_fmt = '\n    - '
    if rhsm_repos:
        api.current_logger().info('The following repoids are available through RHSM:{0}{1}'
                                  .format(list_separator_fmt, list_separator_fmt.join(rhsm_repos)))
    else:
        api.current_logger().info('There are no repos available through RHSM.')
    return rhsm_repos


def _inhibit_on_duplicate_repos(repofiles):
    """
    Inhibit the upgrade if any repoid is defined multiple times.

    When that happens, it not only shows misconfigured system, but then
    we can't get details of all the available repos as well.
    """
    duplicates = repofileutils.get_duplicate_repositories(repofiles).keys()

    if not duplicates:
        return
    list_separator_fmt = '\n    - '
    api.current_logger().warning(
        'The following repoids are defined multiple times:{0}{1}'
        .format(list_separator_fmt, list_separator_fmt.join(duplicates))
    )

    reporting.create_report([
        reporting.Title('A YUM/DNF repository defined multiple times'),
        reporting.Summary(
            'The following repositories are defined multiple times:{0}{1}'
            .format(list_separator_fmt, list_separator_fmt.join(duplicates))
        ),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Tags([reporting.Tags.REPOSITORY]),
        reporting.Flags([reporting.Flags.INHIBITOR]),
        reporting.Remediation(hint='Remove the duplicate repository definitions.')
    ])


@with_rhsm
def get_enabled_repo_ids(context):
    """
    Retrieve repo ids of all the repositories enabled through the subscription-manager.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: Repositories that are enabled on the current system through the subscription-manager.
    :rtype: List(string)
    """
    with _handle_rhsm_exceptions():
        result = context.call(['subscription-manager', 'repos', '--list-enabled'], split=False)
        return _RE_REPO_UID.findall(result['stdout'])


@with_rhsm
@_rhsm_retry(max_attempts=_ATTEMPTS, sleep=_RETRY_SLEEP)
def unset_release(context):
    """
    Unset the configured release from the subscription-manager.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    """
    with _handle_rhsm_exceptions():
        context.call(['subscription-manager', 'release', '--unset'], split=False)


@with_rhsm
@_rhsm_retry(max_attempts=_ATTEMPTS, sleep=_RETRY_SLEEP)
def set_release(context, release):
    """
    Set the release (RHEL minor version) through the subscription-manager.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :param release: Release to set the subscription-manager to.
    :type release: str
    """
    with _handle_rhsm_exceptions():
        context.call(['subscription-manager', 'release', '--set', release], split=False)


@with_rhsm
def get_release(context):
    """
    Retrieves the release the subscription-manager has been pinned to, if applicable.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: Release the subscription-manager is set to.
    :rtype: string
    """
    with _handle_rhsm_exceptions():
        result = context.call(['subscription-manager', 'release'], split=False)
        result = _RE_RELEASE.findall(result['stdout'])
        return result[0] if result else ''


@with_rhsm
@_rhsm_retry(max_attempts=_ATTEMPTS, sleep=_RETRY_SLEEP)
def refresh(context):
    """
    Calls 'subscription-manager refresh'

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    """
    with _handle_rhsm_exceptions():
        context.call(['subscription-manager', 'refresh'], split=False)


@with_rhsm
def get_existing_product_certificates(context):
    """
    Retrieves information about existing product certificates on the system.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: Paths to product certificates that are currently installed on the system.
    :rtype: List(string)
    """
    certs = []
    for path in ('/etc/pki/product', '/etc/pki/product-default'):
        if not os.path.isdir(context.full_path(path)):
            continue
        curr_certs = [os.path.join(path, f) for f in os.listdir(context.full_path(path))
                      if os.path.isfile(os.path.join(context.full_path(path), f))]
        if curr_certs:
            certs.extend(curr_certs)
    return certs


# DO NOT SET the with_rhsm decorator for this function
def set_container_mode(context):
    """
    Put RHSM into the container mode.

    Inside the container, we have to ensure the RHSM is not used AND that host
    is not affected. If the RHSM is not set into the container mode, the host
    could be affected and the generated repo file in the container could be
    affected as well (e.g. when the release is set, using rhsm, on the host).

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    """
    if not context.is_isolated():
        api.current_logger().error('Trying to set RHSM into the container mode'
                                   'on host. Skipping the action.')
        return
    try:
        context.call(['ln', '-s', '/etc/rhsm', '/etc/rhsm-host'])
    except CalledProcessError:
        raise StopActorExecutionError(
                message='Cannot set the container mode for the subscription-manager.')


@with_rhsm
def switch_certificate(context, rhsm_info, cert_path):
    """
    Perform all actions needed to switch the passed RHSM product certificate.

    This function will copy the certificate to /etc/pki/product, and /etc/pki/product-default if necessary, and
    remove other product certificates from there.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :param rhsm_info: An instance of the RHSMInfo model
    :type rhsm_info: RHSMInfo model
    :param cert_path: Path to the product certificate to switch to
    :type cert_path: string
    """
    for existing in rhsm_info.existing_product_certificates:
        try:
            context.remove(existing)
        except OSError:
            api.current_logger().warning('Failed to remove existing certificate: %s', existing, exc_info=True)

    for path in ('/etc/pki/product', '/etc/pki/product-default'):
        if os.path.isdir(context.full_path(path)):
            context.copy_to(cert_path, os.path.join(path, os.path.basename(cert_path)))


@with_rhsm
def scan_rhsm_info(context):
    """
    Gather all the RHSM information of the source system.

    It's not intended for gathering RHSM info about the target system within a container.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: An instance of an RHSMInfo model.
    :rtype: RHSMInfo model
    """
    info = RHSMInfo()
    info.attached_skus = get_attached_skus(context)
    info.available_repos = get_available_repo_ids(context)
    info.enabled_repos = get_enabled_repo_ids(context)
    info.release = get_release(context)
    info.existing_product_certificates.extend(get_existing_product_certificates(context))
    info.sca_detected = is_manifest_sca(context)
    return info


# repos/system_upgrade/common/libraries/rhui.py

import os

import six

from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api

# when on AWS and upgrading from RHEL 7, we need also Python2 version of "Amazon-id" dnf
# plugin which is served by "leapp-rhui-aws" rpm package (please note this package is not
# in any RH official repository but only in "rhui-client-config-*" repo)
DNF_PLUGIN_PATH_PY2 = '/usr/lib/python2.7/site-packages/dnf-plugins/'
YUM_REPOS_PATH = '/etc/yum.repos.d'

RHUI_PKI_DIR = '/etc/pki/rhui'
RHUI_PKI_PRODUCT_DIR = os.path.join(RHUI_PKI_DIR, 'product')
RHUI_PKI_PRIVATE_DIR = os.path.join(RHUI_PKI_DIR, 'private')

AWS_DNF_PLUGIN_NAME = 'amazon-id.py'


# The files in 'files_map' are provided by special Leapp rpms (per cloud) and
# are delivered into "repos/system_upgrade/common/files/rhui/<PROVIDER>


RHUI_CLOUD_MAP = {
    '7to8': {
        'aws': {
            'src_pkg': 'rh-amazon-rhui-client',
            'target_pkg': 'rh-amazon-rhui-client',
            'leapp_pkg': 'leapp-rhui-aws',
            'leapp_pkg_repo': 'leapp-aws.repo',
            'files_map': [
                ('rhui-client-config-server-8.crt', RHUI_PKI_PRODUCT_DIR),
                ('rhui-client-config-server-8.key', RHUI_PKI_DIR),
                ('content-rhel8.crt', RHUI_PKI_PRODUCT_DIR),
                ('content-rhel8.key', RHUI_PKI_DIR),
                ('cdn.redhat.com-chain.crt', RHUI_PKI_DIR),
                (AWS_DNF_PLUGIN_NAME, DNF_PLUGIN_PATH_PY2),
                ('leapp-aws.repo', YUM_REPOS_PATH)
            ],
        },
        'aws-sap-e4s': {
            'src_pkg': 'rh-amazon-rhui-client-sap-bundle',
            'target_pkg': 'rh-amazon-rhui-client-sap-bundle-e4s',
            'leapp_pkg': 'leapp-rhui-aws-sap-e4s',
            'leapp_pkg_repo': 'leapp-aws-sap-e4s.repo',
            'files_map': [
                ('rhui-client-config-server-8-sap-bundle.crt', RHUI_PKI_PRODUCT_DIR),
                ('rhui-client-config-server-8-sap-bundle.key', RHUI_PKI_DIR),
                ('content-rhel8-sap.crt', RHUI_PKI_PRODUCT_DIR),
                ('content-rhel8-sap.key', RHUI_PKI_DIR),
                ('cdn.redhat.com-chain.crt', RHUI_PKI_DIR),
                (AWS_DNF_PLUGIN_NAME, DNF_PLUGIN_PATH_PY2),
                ('leapp-aws-sap-e4s.repo', YUM_REPOS_PATH)
            ],
        },
        'azure': {
            'src_pkg': 'rhui-azure-rhel7',
            'target_pkg': 'rhui-azure-rhel8',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure',
            'leapp_pkg_repo': 'leapp-azure.repo',
            'files_map': [
                ('content.crt', RHUI_PKI_PRODUCT_DIR),
                ('key.pem', RHUI_PKI_PRIVATE_DIR),
                ('leapp-azure.repo', YUM_REPOS_PATH)
            ],
        },
        'azure-sap': {
            'src_pkg': 'rhui-azure-rhel7-base-sap-ha',
            'target_pkg': 'rhui-azure-rhel8-sap-ha',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure-sap',
            'leapp_pkg_repo': 'leapp-azure-sap.repo',
            'files_map': [
                ('content-rhel8-sap-ha.crt', RHUI_PKI_PRODUCT_DIR),
                ('key-rhel8-sap-ha.pem', RHUI_PKI_DIR),
                ('leapp-azure-sap.repo', YUM_REPOS_PATH)
            ],
        },
    },
    '8to9': {
        'aws': {
            'src_pkg': 'rh-amazon-rhui-client-ha',
            'target_pkg': 'rh-amazon-rhui-client',
            'leapp_pkg': 'leapp-rhui-aws',
            'leapp_pkg_repo': 'leapp-aws.repo',
            'files_map': [
                ('rhui-client-config-server-9.crt', RHUI_PKI_PRODUCT_DIR),
                ('rhui-client-config-server-9.key', RHUI_PKI_DIR),
                ('content-rhel9.crt', RHUI_PKI_PRODUCT_DIR),
                ('content-rhel9.key', RHUI_PKI_DIR),
                ('cdn.redhat.com-chain.crt', RHUI_PKI_DIR),
                ('leapp-aws.repo', YUM_REPOS_PATH)
            ],
        },
        # not yet enabled
        'aws-sap-e4s': {
            'src_pkg': 'rh-amazon-rhui-client-sap-bundle',
            'target_pkg': 'rh-amazon-rhui-client-sap-bundle-e4s',
            'leapp_pkg': 'leapp-rhui-aws-sap-e4s',
            'leapp_pkg_repo': 'leapp-aws-sap-e4s.repo',
            'files_map': [
                ('rhui-client-config-server-9-sap-bundle.crt', RHUI_PKI_PRODUCT_DIR),
                ('rhui-client-config-server-9-sap-bundle.key', RHUI_PKI_DIR),
                ('content-rhel9-sap.crt', RHUI_PKI_PRODUCT_DIR),
                ('content-rhel9-sap.key', RHUI_PKI_DIR),
                ('cdn.redhat.com-chain.crt', RHUI_PKI_DIR),
                (AWS_DNF_PLUGIN_NAME, DNF_PLUGIN_PATH_PY2),
                ('leapp-aws-sap-e4s.repo', YUM_REPOS_PATH)
            ],
        },
        # not yet enabled
        'azure': {
            'src_pkg': 'rhui-azure-rhel8',
            'target_pkg': 'rhui-azure-rhel9',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure',
            'leapp_pkg_repo': 'leapp-azure.repo',
            'files_map': [
                ('content.crt', RHUI_PKI_PRODUCT_DIR),
                ('key.pem', RHUI_PKI_PRIVATE_DIR),
                ('leapp-azure.repo', YUM_REPOS_PATH)
            ],
        },
        # not yet enabled
        'azure-sap': {
            'src_pkg': 'rhui-azure-rhel8-base-sap-ha',
            'target_pkg': 'rhui-azure-rhel9-sap-ha',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure-sap',
            'leapp_pkg_repo': 'leapp-azure-sap.repo',
            'files_map': [
                ('content-rhel9-sap-ha.crt', RHUI_PKI_PRODUCT_DIR),
                ('key-rhel9-sap-ha.pem', RHUI_PKI_DIR),
                ('leapp-azure-sap.repo', YUM_REPOS_PATH)
            ],
        },
    },
}


def get_upg_path():
    """
    Get upgrade path in specific string format
    """
    return '7to8' if get_target_major_version() == '8' else '8to9'


def gen_rhui_files_map():
    """
    Generate RHUI files map based on architecture and upgrade path
    """
    arch = api.current_actor().configuration.architecture
    upg_path = get_upg_path()

    cloud_map = RHUI_CLOUD_MAP
    # for the moment the only arch related difference in RHUI package naming is on ARM
    if arch == 'aarch64':
        cloud_map[get_upg_path()]['aws']['src_pkg'] = 'rh-amazon-rhui-client-arm'

    files_map = dict((k, v['files_map']) for k, v in six.iteritems(cloud_map[upg_path]))
    return files_map


def copy_rhui_data(context, provider):
    """
    Copy relevant RHUI cerificates and key into the target userspace container
    """
    rhui_dir = api.get_common_folder_path('rhui')
    data_dir = os.path.join(rhui_dir, provider)

    context.call(['mkdir', '-p', RHUI_PKI_PRODUCT_DIR])
    context.call(['mkdir', '-p', RHUI_PKI_PRIVATE_DIR])

    for path_ in gen_rhui_files_map().get(provider, ()):
        context.copy_to(os.path.join(data_dir, path_[0]), path_[1])


# repos/system_upgrade/common/libraries/rpms.py

from leapp.libraries import stdlib
from leapp.models import InstalledRPM


def get_installed_rpms():
    rpm_cmd = [
        '/bin/rpm',
        '-qa',
        '--queryformat',
        r'%{NAME}|%{VERSION}|%{RELEASE}|%|EPOCH?{%{EPOCH}}:{0}||%|PACKAGER?{%{PACKAGER}}:{(none)}||%|'
        r'ARCH?{%{ARCH}}:{}||%|DSAHEADER?{%{DSAHEADER:pgpsig}}:{%|RSAHEADER?{%{RSAHEADER:pgpsig}}:{(none)}|}|\n'
    ]
    try:
        return stdlib.run(rpm_cmd, split=True)['stdout']
    except stdlib.CalledProcessError as err:
        error = 'Execution of {CMD} returned {RC}. Unable to find installed packages.'.format(CMD=err.command,
                                                                                              RC=err.exit_code)
        stdlib.api.current_logger().error(error)
        return []


def create_lookup(model, field, keys, context=stdlib.api):
    """
    Create a lookup set from one of the model fields.

    :param model: model class
    :param field: model field, its value will be taken for lookup data
    :param key: property of the field's data that will be used to build a resulting set
    :param context: context of the execution
    """
    data = getattr(next((m for m in context.consume(model)), model()), field)
    try:
        return {tuple(getattr(obj, key) for key in keys) for obj in data} if data else set()
    except TypeError:
        # data is not iterable, not lookup can be built
        stdlib.api.current_logger().error(
                "{model}.{field}.{keys} is not iterable, can't build lookup".format(
                    model=model, field=field, keys=keys))
        return set()


def has_package(model, package_name, arch=None, context=stdlib.api):
    """
    Expects a model InstalledRedHatSignedRPM or InstalledUnsignedRPM.
    Can be useful in cases like a quick item presence check, ex. check in actor that
    a certain package is installed.

    :param model: model class
    :param package_name: package to be checked
    :param arch: filter by architecture. None means all arches.
    """
    if not (isinstance(model, type) and issubclass(model, InstalledRPM)):
        return False
    keys = ('name',) if not arch else ('name', 'arch')
    rpm_lookup = create_lookup(model, field='items', keys=keys, context=context)
    return (package_name, arch) in rpm_lookup if arch else (package_name,) in rpm_lookup


# repos/system_upgrade/common/libraries/testutils.py

from collections import namedtuple
import logging
import os

from leapp.libraries.common.config import architecture
from leapp.models import EnvVar
from leapp.utils.deprecation import deprecated


class produce_mocked(object):
    def __init__(self):
        self.called = 0
        self.model_instances = []

    def __call__(self, *model_instances):
        self.called += len(model_instances)
        self.model_instances.extend(list(model_instances))


class create_report_mocked(object):
    def __init__(self):
        self.called = 0
        self.report_fields = {}

    def __call__(self, report_fields):
        self.called += 1
        # iterate list of report primitives (classes)
        for report in report_fields:
            # last element of path is our field name
            self.report_fields.update(report.to_dict())


class logger_mocked(object):
    def __init__(self):
        self.dbgmsg = []
        self.infomsg = []
        self.warnmsg = []
        self.errmsg = []

    def debug(self, *args):
        self.dbgmsg.extend(args)

    def info(self, *args):
        self.infomsg.extend(args)

    @deprecated(since='2020-09-23', message=(
        'The logging.warn method has been deprecated since Python 3.3.'
        'Use the warning method instead.'
    ))
    def warn(self, *args):
        self.warnmsg.extend(args)

    def warning(self, *args):
        self.warnmsg.extend(args)

    def error(self, *args):
        self.errmsg.extend(args)

    def __call__(self):
        return self


class CurrentActorMocked(object):  # pylint:disable=R0904
    def __init__(self, arch=architecture.ARCH_X86_64, envars=None, kernel='3.10.0-957.43.1.el7.x86_64',
                 release_id='rhel', src_ver='7.8', dst_ver='8.1', msgs=None, flavour='default'):
        envarsList = [EnvVar(name=k, value=v) for k, v in envars.items()] if envars else []
        version = namedtuple('Version', ['source', 'target'])(src_ver, dst_ver)
        release = namedtuple('OS_release', ['release_id', 'version_id'])(release_id, src_ver)

        self._common_folder = '../../files'
        self._actor_folder = 'files'
        self.configuration = namedtuple(
            'configuration', ['architecture', 'kernel', 'leapp_env_vars', 'os_release', 'version', 'flavour']
        )(arch, kernel, envarsList, release, version, flavour)
        self._msgs = msgs or []

    def __call__(self):
        return self

    def get_common_folder_path(self, folder):
        return os.path.join(self._common_folder, folder)

    def consume(self, model):
        return iter(filter(  # pylint:disable=W0110,W1639
            lambda msg: isinstance(msg, model), self._msgs
        ))

    @property
    def log(self):
        return logging.getLogger(__name__)

    def get_actor_file_path(self, name):
        return os.path.join(self._actor_folder, name)

    # other functions and properties from the API - can be implemented as needed

    def serialize(self):
        raise NotImplementedError

    def get_answers(self, dialog):
        raise NotImplementedError

    def show_message(self, message):
        raise NotImplementedError

    @property
    def actor_files_paths(self):
        raise NotImplementedError

    @property
    def files_paths(self):
        raise NotImplementedError

    @property
    def common_files_paths(self):
        raise NotImplementedError

    @property
    def actor_tools_paths(self):
        raise NotImplementedError

    @property
    def common_tools_paths(self):
        raise NotImplementedError

    @property
    def tools_paths(self):
        raise NotImplementedError

    def get_folder_path(self, name):
        raise NotImplementedError

    def get_actor_folder_path(self, name):
        raise NotImplementedError

    def get_file_path(self, name):
        raise NotImplementedError

    def get_common_file_path(self, name):
        raise NotImplementedError

    def get_tool_path(self, name):
        raise NotImplementedError

    def get_common_tool_path(self, name):
        raise NotImplementedError

    def get_actor_tool_path(self, name):
        raise NotImplementedError

    def run(self, *args):
        raise NotImplementedError

    def produce(self, *models):
        raise NotImplementedError

    def report_error(self, message, severity, details):
        raise NotImplementedError


def make_IOError(error):
    """
    Create an IOError instance

    Create an IOError instance with the given error number.

    :param error: the error number, e.g. errno.ENOENT
    """
    return IOError(error, os.strerror(error))


def make_OSError(error):
    """
    Create an OSError instance

    Create an OSError instance with the given error number.

    :param error: the error number, e.g. errno.ENOENT
    """
    return OSError(error, os.strerror(error))


# repos/system_upgrade/common/libraries/utils.py

import functools
import os
import sys

import six

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import mounting
from leapp.libraries.stdlib import api, CalledProcessError, config, run, STDOUT


def parse_config(cfg=None, strict=True):
    """
    Applies a workaround to parse a config file using py3 AND py2

    ConfigParser has a new def to read strings/iles in Py3, making
    the old ones (Py2) obsoletes, these function was created to use the
    ConfigParser on Py2 and Py3

    :type cfg: str
    :type strict: bool
    """
    if six.PY3:
        parser = six.moves.configparser.ConfigParser(strict=strict)  # pylint: disable=unexpected-keyword-arg
    else:
        parser = six.moves.configparser.ConfigParser()

    # we do not handle exception here, handle with it when these function is called
    if cfg and six.PY3:
        # Python 3
        if isinstance(cfg, six.string_types):
            parser.read_string(cfg)
        else:
            parser.read_file(cfg)
    elif cfg:
        # Python 2
        from cStringIO import StringIO  # pylint: disable=import-outside-toplevel
        if isinstance(cfg, six.string_types):
            parser.readfp(StringIO(cfg))  # pylint: disable=deprecated-method
        else:
            parser.readfp(cfg)  # pylint: disable=deprecated-method
    return parser


def makedirs(path, mode=0o777, exists_ok=True):
    mounting._makedirs(path=path, mode=mode, exists_ok=exists_ok)


def apply_yum_workaround(context=None):
    """
    Applies a workaround on the system to allow the upgrade to succeed for yum/dnf.
    """
    yum_script_path = api.get_tool_path('handleyumconfig')
    if not yum_script_path:
        raise StopActorExecutionError(
            message='Failed to find mandatory script to apply',
            details=reinstall_leapp_repository_hint()
        )
    cmd = ['/bin/bash', '-c', yum_script_path]

    try:
        context = context or mounting.NotIsolatedActions(base_dir='/')
        context.call(cmd)
    except OSError as e:
        raise StopActorExecutionError(
            message='Failed to exceute script to apply yum adjustment. Message: {}'.format(str(e))
        )
    except CalledProcessError as e:
        raise StopActorExecutionError(
            message='Failed to apply yum adjustment. Message: {}'.format(str(e))
        )


def logging_handler(fd_info, buf):
    """
    Custom log handler to always show stdout to console and stderr only in DEBUG mode
    """
    (_unused, fd_type) = fd_info
    if fd_type != STDOUT and not config.is_debug():
        return
    target = sys.stdout if fd_type == STDOUT else sys.stderr
    if sys.version_info > (3, 0):
        os.writev(target.fileno(), [buf])
    else:
        target.write(buf)


def reinstall_leapp_repository_hint():
    """
    Convenience function for creating a detail for StopActorExecutionError with a hint to reinstall the
    leapp-repository package
    """
    return {
        'hint': 'Try to reinstall the `leapp-repository` package.'
    }


def report_and_ignore_shutil_rmtree_error(func, path, exc_info):
    """
    Helper function for shutil.rmtree to only report errors but don't fail.
    """
    api.current_logger().warning(
        'While trying to remove directories: %s failed at %s with an exception %s message: %s',
        func.__name__, path, exc_info[0].__name__, exc_info[1]
    )


def call_with_oserror_handled(cmd):
    """
    Perform run with already handled OSError for some convenience.
    """
    try:
        run(cmd)
    except OSError as e:
        if cmd:
            raise StopActorExecutionError(
                message=str(e),
                details={
                    'hint': 'Please ensure that {} is installed and executable.'.format(cmd[0])
                }
            )
        raise StopActorExecutionError(
            message='Failed to execute command {} with: {}'.format(''.join(cmd), str(e))
        )


def call_with_failure_hint(cmd, hint):
    """
    Perform `run` which handles OSError through call_with_oserror_handled and transforms CalledProcessError to a
    StopActorExecutionError with a hint given by the caller.
    """
    try:
        call_with_oserror_handled(cmd)
    except CalledProcessError as e:
        raise StopActorExecutionError(
            message='Failed to execute command `{}`. Error: {}'.format(' '.join(cmd), str(e)),
            details={hint: hint}
        )


def clean_guard(cleanup_function):
    """
    Decorator to handle any exception going through and running cleanup tasks through the given cleanup_function
    parameter.
    """

    def clean_wrapper(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except Exception:  # Broad exception handler to handle all cases but rethrows
                api.current_logger().debug('Clean guard caught an exception - Calling cleanup function.')
                try:
                    cleanup_function(*args, **kwargs)
                except Exception:  # pylint: disable=broad-except
                    # Broad exception handler to handle all cases however, swallowed, to avoid loosing the original
                    # error. Logging for debuggability.
                    api.current_logger().warning('Caught and swallowed an exception during cleanup.', exc_info=True)
                raise  # rethrow original exception

        return wrapper

    return clean_wrapper


def read_file(path):
    """
    Reads the file specified by path in text mode and returns the contents.
    """
    with open(path, 'r') as f:
        return f.read()


# repos/system_upgrade/common/models/activekernelmodulesfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class KernelModuleParameter(Model):
    topic = SystemFactsTopic

    name = fields.String()
    value = fields.String()


class ActiveKernelModule(Model):
    topic = SystemFactsTopic

    filename = fields.String()
    parameters = fields.List(fields.Model(KernelModuleParameter))
    signature = fields.Nullable(fields.String())


class ActiveKernelModulesFacts(Model):
    topic = SystemFactsTopic

    kernel_modules = fields.List(fields.Model(ActiveKernelModule))


# repos/system_upgrade/common/models/bootcontent.py

from leapp.models import Model, fields
from leapp.topics import BootPrepTopic


class BootContent(Model):
    """
    For information about what Leapp copies to the /boot/. We need to pass this information
    at least to the actors performing /boot/ cleanup.
    """
    topic = BootPrepTopic

    kernel_path = fields.String(help='Filepath of the kernel copied to /boot/ by Leapp.')
    initram_path = fields.String(help='Filepath of the initramfs copied to /boot/ by Leapp.')


# repos/system_upgrade/common/models/cpuinfo.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class CPUInfo(Model):
    """
    The model represents information about CPUs.

    The model currently doesn't represent all information about cpus could
    provide on the machine. Just part of them, in case any other attributes
    will be neded, the model can be extended.

    The provided info is aggregated - like from lscpu command. Expecting all
    CPUs are same on the machine (at least for now).
    """

    topic = SystemFactsTopic

    machine_type = fields.Nullable(fields.Integer())
    """
    Specifies machine type if provided.

    This is important for the check of s390x, whether the HW is supported
    by RHEL 8.
    """

    # TODO: regarding possible problems with LOCALE, I am keeping most of
    # parts commented out and focus just on the one particular needed info.
    # architecture = fields.String()
    # """ Architecture of the CPU (e.g. x86_64) """

    # byte_order = fields.StringEnum(['Little Endian', 'Big Endian'])
    # """ Byte order of the CPU: 'Little Endian' or 'Big Endian' """

    # flags = fields.List(fields.String(), default=[])
    # """ Specifies flags/features of the CPU. """

    # hypervisor = fields.Nullable(fields.String())
    # hypervisor_vendor = fields.Nullable(fields.String())

    # number = fields.Integer()
    # """ Number of CPUs. """

    # vendor_id = fields.Nullable(fields.String())
    # """ ID of vendor of the CPU. """

    # virtualization = fields.Nullable(fields.String())
    # virtualization_type = fields.Nullable(fields.String())


# repos/system_upgrade/common/models/defaultgrubinfo.py

from leapp.models import fields, Model
from leapp.topics import SystemFactsTopic


class DefaultGrub(Model):
    """
    A model with '/etc/default/grub' option as key / value
    """
    topic = SystemFactsTopic

    name = fields.String()
    """ Option name, e.g. GRUB_TIMEOUT """
    value = fields.String()
    """ Option value, e.g. '5' in case of GRUB_TIMEOUT=5 """


class DefaultGrubInfo(Model):
    """
    A message with '/etc/default/grub' content
    """

    topic = SystemFactsTopic

    default_grub_info = fields.List(fields.Model(DefaultGrub))
    """ List of '/etc/default/grub' options as key / value """


# repos/system_upgrade/common/models/dnfplugintask.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class DNFPluginTask(Model):
    """
    Represents information what should DNF do with a specifiec DNF plugin
    in various stages.

    Currently, it's possible just to disable specified DNF plugins.

    Available stages: "check", "download" and "upgrade
    """

    topic = SystemInfoTopic

    name = fields.String()
    disable_in = fields.List(fields.String(), default=['check', 'download', 'upgrade'])


# repos/system_upgrade/common/models/firewallsfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class FirewallStatus(Model):
    topic = SystemFactsTopic

    enabled = fields.Boolean()
    active = fields.Boolean()


class FirewallsFacts(Model):
    topic = SystemFactsTopic

    firewalld = fields.Model(FirewallStatus)
    iptables = fields.Model(FirewallStatus)
    ip6tables = fields.Model(FirewallStatus)


# repos/system_upgrade/common/models/firmwarefacts.py

from leapp.models import fields, Model
from leapp.topics import SystemFactsTopic


class FirmwareFacts(Model):
    topic = SystemFactsTopic

    firmware = fields.StringEnum(['bios', 'efi'])
    """ System firmware interface (BIOS or EFI) """
    ppc64le_opal = fields.Nullable(fields.Boolean())
    """ Check OPAL presence to identify ppc64le bare metal systems """


# repos/system_upgrade/common/models/groupsfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class Group(Model):
    topic = SystemFactsTopic

    name = fields.String()
    gid = fields.Integer()
    members = fields.List(fields.String())


class GroupsFacts(Model):
    topic = SystemFactsTopic

    groups = fields.List(fields.Model(Group))


# repos/system_upgrade/common/models/grubcfgbios.py

from leapp.models import fields, Model
from leapp.topics import SystemFactsTopic


class GrubCfgBios(Model):
    """
    A message providing info about BIOS (non-EFI) GRUB config
    """
    topic = SystemFactsTopic

    insmod_bls = fields.Boolean()
    """ Inform whether blscfg is loaded """


# repos/system_upgrade/common/models/grubconfigerror.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class GrubConfigError(Model):
    topic = SystemFactsTopic

    error_detected = fields.Boolean(default=False)


# repos/system_upgrade/common/models/grubdevice.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic
from leapp.utils.deprecation import deprecated


@deprecated(
    since='2020-09-01',
    message=(
        'The model is deprecated as the current implementation was not reliable. '
        'We moved the GRUB device detection into grub library. '
        'Please use get_grub_device() function instead.'
    )
)
class GrubDevice(Model):
    topic = SystemFactsTopic

    grub_device = fields.String()


@deprecated(
    since='2020-09-01',
    message=(
        'The model is deprecated as the current implementation was not reliable. '
        'We moved the GRUB device detection into grub library. '
        'Please use get_grub_device() function instead.'
    )
)
class UpdateGrub(Model):
    topic = SystemFactsTopic

    grub_device = fields.String()


# repos/system_upgrade/common/models/grubenv.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class HybridImage(Model):
    """
    Model used for instructing Leapp to convert "grubenv" symlink
    into a regular file in case of hybrid (BIOS/EFI) images using BIOS
    on Azure.
    """
    topic = SystemFactsTopic
    detected = fields.Boolean(default=False)


# repos/system_upgrade/common/models/initramfs.py

from leapp.models import Model, fields
from leapp.topics import BootPrepTopic, SystemInfoTopic
from leapp.utils.deprecation import deprecated


class DracutModule(Model):
    """
    Specify a dracut module that should be included into the initramfs

    The specified dracut module has to be compatible with the target system.

    See the description of UpgradeInitramfsTasks and TargetInitramfsTasks
    for more information about the role of initramfs in the in-place upgrade
    process.
    """
    topic = BootPrepTopic

    name = fields.String()
    """
    Name of the dracut module that should be added (--add option of dracut)
    when a initramfs is built.
    """

    module_path = fields.Nullable(fields.String(default=None))
    """
    module_path specifies dracut modules that are supposed to be copied

    If the path is not set, the given name will just be activated. IOW,
    if the dracut module is stored outside the /usr/lib/dracut/modules.d/
    directory, set the absolute path to it, so leapp will manage it during
    the upgrade to ensure the module will be added into the initramfs.

    The module has to be stored on the local storage. In such a case, it is
    recommended to store it into the 'files' directory of an actor generating
    this object.

    Note: It's expected to set the full path from the host POV. In case
    of actions inside containers, the module is still copied from the HOST
    into the container workspace.
    """


class UpgradeInitramfsTasks(Model):
    """
    Influence generating of the (leapp) upgrade initramfs

    The upgrade initramfs is used during the crucial part of the upgrade,
    in which the original rpms are upgraded, configuration of applications
    are migrated, etc. To be able to boot into the leapp upgrade environment
    correctly, it is expected all needed drivers, configuration files, ... are
    included inside the upgrade initramfs. Produce this message with
    expected content to influence the upgrade initramfs.

    If some specific rpms or content is required to be able to build the
    upgrade initramfs, see the <container-model>.

    Note: The built initramfs is composed of stuff for the target system.
    In example, if you are on RHEL 7 and plan the upgrade to RHEL 8, you need
    to provide content (e.g. drivers, dracut modules) compatible with
    RHEL 8 system.
    """
    topic = BootPrepTopic

    include_files = fields.List(fields.String(), default=[])
    """
    List of files (cannonical filesystem paths) to include in the initramfs
    """

    include_dracut_modules = fields.List(fields.Model(DracutModule), default=[])
    """
    List of dracut modules that should be installed in the initramfs.

    See the DracutModule model for more information.
    """


class TargetInitramfsTasks(UpgradeInitramfsTasks):
    """
    Analogy to UpgradeInitramfsTasks, but referring to the target initram disk.

    Target initramfs is the one, that will be used to boot to your upgraded
    system. If you want to ensure that you are able to boot into the target
    (upgraded) system, it is possible you need to add same stuff as you added
    into the upgrade initramfs.

    If some specific rpms are required to be able to build the upgrade
    initramfs, install these via the RpmTransactionTasks model.
    """


@deprecated(since='2021-10-10', message='Replaced by TargetInitramfsTasks.')
class InitrdIncludes(Model):
    """
    List of files (cannonical filesystem paths) to include in RHEL-8 initramfs
    """
    topic = SystemInfoTopic

    files = fields.List(fields.String())


@deprecated(since='2021-10-10', message='Replaced by UpgradeInitramfsTasks.')
class UpgradeDracutModule(Model):
    """
    Specify a dracut module that should be included into the (leapp) upgrade initramfs.

    The upgrade initramfs is used during the crucial part of the upgrade,
    in which the original rpms are upgraded. If a dracut module is required to
    be included inside the upgrade initramfs (e.g. because it is needed
    to handle/initialize your storage properly), produce this msg.
    """
    topic = BootPrepTopic

    name = fields.String()
    """
    Name of the dracut module that should be added (--add option of dracut)
    """

    module_path = fields.Nullable(fields.String(default=None))
    """
    module_path specifies dracut modules that are to be copied

    If the path is not set, the given name will just be activated.
    """


# repos/system_upgrade/common/models/installeddesktopsfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class InstalledDesktopsFacts(Model):
    """
    The model includes fact about installe
    """
    topic = SystemFactsTopic
    gnome_installed = fields.Boolean(default=False)
    kde_installed = fields.Boolean(default=False)


# repos/system_upgrade/common/models/installedkernelversion.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class CurrentKernel(Model):
    topic = SystemInfoTopic
    version = fields.String()
    release = fields.String()
    arch = fields.String()


# repos/system_upgrade/common/models/installedrpm.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class RPM(Model):
    topic = SystemInfoTopic
    name = fields.String()
    epoch = fields.String()
    packager = fields.String()
    version = fields.String()
    release = fields.String()
    arch = fields.String()
    pgpsig = fields.String()
    repository = fields.Nullable(fields.String())
    module = fields.Nullable(fields.String())
    stream = fields.Nullable(fields.String())


class InstalledRPM(Model):
    topic = SystemInfoTopic
    items = fields.List(fields.Model(RPM), default=[])


class InstalledRedHatSignedRPM(InstalledRPM):
    pass


class InstalledUnsignedRPM(InstalledRPM):
    pass


# repos/system_upgrade/common/models/installedtargetkernelversion.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class InstalledTargetKernelVersion(Model):
    """
    This message is used to propagate the version of the kernel that has been installed during the upgrade process.

    The version value is mainly used for boot loader entry manipulations, to know which boot entry to manipulate.
    """
    topic = SystemInfoTopic
    version = fields.String()


# repos/system_upgrade/common/models/ipainfo.py

from leapp.models import fields, Model
from leapp.topics import SystemFactsTopic


class IpaInfo(Model):
    topic = SystemFactsTopic

    has_client_package = fields.Boolean()
    is_client_configured = fields.Boolean()

    has_server_package = fields.Boolean()
    is_server_configured = fields.Boolean()


# repos/system_upgrade/common/models/ipuconfig.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class EnvVar(Model):
    topic = SystemInfoTopic

    name = fields.String()
    """Name of the environment variable."""

    value = fields.String()
    """Value of the environment variable."""


class OSRelease(Model):
    topic = SystemInfoTopic

    release_id = fields.String()
    name = fields.String()
    pretty_name = fields.String()
    version = fields.String()
    version_id = fields.String()
    variant = fields.Nullable(fields.String())  # isn't specified on some systems
    variant_id = fields.Nullable(fields.String())  # same as above


class Version(Model):
    topic = SystemInfoTopic

    source = fields.String()
    """Version of the source (current) system. E.g.: '7.8'."""

    target = fields.String()
    """Version of the target system. E.g. '8.2.'."""


class IPUConfig(Model):
    """
    IPU workflow configuration model
    """
    topic = SystemInfoTopic

    leapp_env_vars = fields.List(fields.Model(EnvVar), default=[])
    """Environment variables related to the leapp."""

    os_release = fields.Model(OSRelease)
    """Data about the OS get from /etc/os-release."""

    version = fields.Model(Version)
    """Version of the current (source) system and expected target system."""

    architecture = fields.String()
    """Architecture of the system. E.g.: 'x86_64'."""

    kernel = fields.String()
    """Originally booted kernel when on the source system."""

    flavour = fields.StringEnum(('default', 'saphana'), default='default')
    """Flavour of the upgrade - Used to influence changes in supported source/target release"""


# repos/system_upgrade/common/models/kernelcmdlineargs.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class KernelCmdlineArg(Model):
    """
    Single kernel command line parameter with/without a value

    When produced alone, represents a parameter to include in RHEL-8 kernel cmdline.
    """
    topic = SystemInfoTopic

    key = fields.String()
    value = fields.Nullable(fields.String())


class KernelCmdline(Model):
    """
    Kernel command line parameters the system was booted with
    """
    topic = SystemInfoTopic

    parameters = fields.List(fields.Model(KernelCmdlineArg))


# repos/system_upgrade/common/models/leftoverpackages.py

from leapp.models import Model, fields, RPM
from leapp.topics import TransactionTopic


class LeftoverPackages(Model):
    topic = TransactionTopic
    items = fields.List(fields.Model(RPM), default=[])


class RemovedPackages(LeftoverPackages):
    pass


# repos/system_upgrade/common/models/memoryinfo.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class MemoryInfo(Model):
    """
    Represents information about available memory (KiB)
    """

    topic = SystemFactsTopic

    mem_total = fields.Nullable(fields.Integer())
    """
    Total memory in KiB
    """


# repos/system_upgrade/common/models/module.py

from leapp.models import fields, Model
from leapp.topics import SystemFactsTopic


class Module(Model):
    """
    A single DNF module indentified by its name and stream.
    """
    topic = SystemFactsTopic
    name = fields.String()
    stream = fields.String()


class EnabledModules(Model):
    """
    DNF modules enabled on the source system.
    """
    topic = SystemFactsTopic

    modules = fields.List(fields.Model(Module), default=[])


# repos/system_upgrade/common/models/multipathconffacts.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class MultipathConfigOption(Model):
    """Model representing information about a multipath configuration option"""
    topic = SystemInfoTopic

    name = fields.String(default='')
    value = fields.String(default='')


class MultipathConfig(Model):
    """Model representing information about a multipath configuration file"""
    topic = SystemInfoTopic

    pathname = fields.String()
    """Config file path name"""

    default_path_checker = fields.Nullable(fields.String())
    config_dir = fields.Nullable(fields.String())
    """Values of path_checker and config_dir in the defaults section.
       None if not set"""

    default_retain_hwhandler = fields.Nullable(fields.Boolean())
    default_detect_prio = fields.Nullable(fields.Boolean())
    default_detect_checker = fields.Nullable(fields.Boolean())
    reassign_maps = fields.Nullable(fields.Boolean())
    """True if retain_attached_hw_handler, detect_prio, detect_path_checker,
       or reassign_maps is set to "yes" in the defaults section. False
       if set to "no". None if not set."""

    hw_str_match_exists = fields.Boolean(default=False)
    ignore_new_boot_devs_exists = fields.Boolean(default=False)
    new_bindings_in_boot_exists = fields.Boolean(default=False)
    unpriv_sgio_exists = fields.Boolean(default=False)
    detect_path_checker_exists = fields.Boolean(default=False)
    overrides_hwhandler_exists = fields.Boolean(default=False)
    overrides_pg_timeout_exists = fields.Boolean(default=False)
    queue_if_no_path_exists = fields.Boolean(default=False)
    all_devs_section_exists = fields.Boolean(default=False)
    """True if hw_str_match, ignore_new_boot_devs, new_bindings_in_boot,
       detect_path_checker, or unpriv_sgio is set in any section,
       if queue_if_no_path is included in the features line in any
       section or if hardware_handler or pg_timeout is set in the
       overrides section. False otherwise"""

    all_devs_options = fields.List(fields.Model(MultipathConfigOption),
                                   default=[])
    """options in an all_devs device configuration section to be converted to
       an overrides section"""


class MultipathConfFacts(Model):
    """Model representing information from multipath configuration files"""
    topic = SystemInfoTopic

    configs = fields.List(fields.Model(MultipathConfig), default=[])
    """List of multipath configuration files"""


# repos/system_upgrade/common/models/packagemanagerinfo.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class PkgManagerInfo(Model):
    """
    Package manager (yum/dnf) related info

    We expect to have only one single message of this kind produced
    """
    topic = SystemInfoTopic

    etc_releasever = fields.Nullable(fields.String())
    """
    Contain the first line of /etc/{yum,dnf}/vars/releasever file or None if the file does not exist.

    In case the value is empty string, it means the file exists but it is empty. In such a case the
    original configuration is obviously broken.
    """


# repos/system_upgrade/common/models/pcidevices.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class PCIDevice(Model):
    """
    Model to represent a PCI Device.

    There is the following match between model parameters and
    the fields of the output of a shell command `lspci -vmmk`

    slot - 'Slot',
    dev_cls - 'Class',
    vendor - 'Vendor',
    name - 'Device',
    subsystem_vendor - 'SVendor',
    subsystem_name - 'SDevice',
    physical_slot - 'PhySlot',
    rev - 'Rev',
    progif - 'ProgIf',
    driver - 'Driver',
    modules - 'Module',
    numa_node - 'NUMANode',

    pci_id - represents the numeric identification of the device, formed as
        string concatenating of the numeric device identifiers for fields
        Vendor, Device, SVendor, SDevice (output
        of a shell command `lspci -vmmkn`) with the `:` delimiter.
        For example:
        one device from output of `lspci -vmmkn` is:

        ```
        Slot:	04:00.0
        Class:	0880
        Vendor:	8086
        Device:	15bf
        SVendor:	17aa
        SDevice:	2279
        Rev:	01
        Driver:	thunderbolt
        Module:	thunderbolt
        ```

        then
        pci_id == "8086:15bf:17aa:2279"
    """
    topic = SystemInfoTopic

    slot = fields.String()
    dev_cls = fields.String()
    vendor = fields.String()
    name = fields.String()
    subsystem_vendor = fields.Nullable(fields.String())
    subsystem_name = fields.Nullable(fields.String())
    physical_slot = fields.Nullable(fields.String())
    rev = fields.Nullable(fields.String())
    progif = fields.Nullable(fields.String())
    driver = fields.Nullable(fields.String())
    modules = fields.Nullable(fields.List(fields.String()))
    numa_node = fields.Nullable(fields.String())
    pci_id = fields.String()


class PCIDevices(Model):
    topic = SystemInfoTopic

    devices = fields.List(fields.Model(PCIDevice))


class RestrictedPCIDevice(Model):
    """
    Model to represent known restrictions of the given PCI devices.


    pci_id - unsupported pci_ids. It has the following
        structure: {Vendor}:{Device}:{SVendor}:{SDevice}, where all these 4
        parameters are numeric ids (see shell command spci -vmmkn). If SVendor
        and SDevice fields do not exist, then pci_id has the following structure:
        {Vendor}:{Device}.
    driver_name - the name of restricted driver
    device_name - the name of restricted device
    supported_{rhel_version} - 1 is supported on the given RHEL, 0 - not
        supported
    available_{rhel_version} - 1 is available on the given RHEL, 0 - not
        available. it could be the driver is available, but not supported
    comments - the field for comments
    """
    topic = SystemInfoTopic

    pci_id = fields.Nullable(fields.String())
    driver_name = fields.Nullable(fields.String())
    device_name = fields.Nullable(fields.String())
    available_rhel7 = fields.Integer()
    supported_rhel7 = fields.Integer()
    available_rhel8 = fields.Integer()
    supported_rhel8 = fields.Integer()
    available_rhel9 = fields.Integer()
    supported_rhel9 = fields.Integer()
    comment = fields.Nullable(fields.String())


class RestrictedPCIDevices(Model):
    """
    Model to represent all known restricted PCI devices.

    Restricted devices might be restricted based on either
     - pci id (each particular device)
     - driver name (all the family of the devices being served by the
        particular driver).

    However the data type, which represents how the pci id or driver name is
    restricted is identical.

    Thus both attributes has the same data type.

    driver_names - is a container with the devices, which are restricted by
        the driver name identifier
    pci_ids - is a container with the devices, which are restricted by
        the pci_id identifier
    """
    topic = SystemInfoTopic

    driver_names = fields.List(
        fields.Model(RestrictedPCIDevice),
    )
    pci_ids = fields.List(
        fields.Model(RestrictedPCIDevice),
    )


# repos/system_upgrade/common/models/persistentnetnamesfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class PCIAddress(Model):
    """
    TODO: tbd
    """
    topic = SystemInfoTopic

    domain = fields.String()
    bus = fields.String()
    function = fields.String()
    device = fields.String()


class Interface(Model):
    """
    TODO: tbd - Interface or NetworkInterface?
    """
    topic = SystemInfoTopic

    name = fields.String()
    devpath = fields.String()
    driver = fields.String()
    vendor = fields.String()
    pci_info = fields.Model(PCIAddress)
    mac = fields.String()


class PersistentNetNamesFacts(Model):
    """
    Information about network interfaces gather from the original system
    """
    topic = SystemInfoTopic
    interfaces = fields.List(fields.Model(Interface))


class PersistentNetNamesFactsInitramfs(PersistentNetNamesFacts):
    """
    Information about network interfaces gather from initramfs with the kernel of target system
    """
    pass


class RenamedInterface(Model):
    """
    Provide original and new name of the network interface when renamed
    """
    topic = SystemInfoTopic

    rhel7_name = fields.String()
    rhel8_name = fields.String()


class RenamedInterfaces(Model):
    """
    Provide list of renamed network interfaces

    These interfaces will use different names on the target system
    in comparison with original names.
    """
    topic = SystemInfoTopic

    renamed = fields.List(fields.Model(RenamedInterface))


# repos/system_upgrade/common/models/repositoriesblacklisted.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class RepositoriesBlacklisted(Model):
    """
    Repositories ID that should be ignored by Leapp during upgrade process
    """
    topic = SystemFactsTopic

    repoids = fields.List(fields.String(), default=[])


# repos/system_upgrade/common/models/repositoriesfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic
from leapp.utils.deprecation import deprecated


class RepositoryData(Model):
    topic = SystemFactsTopic

    repoid = fields.String()
    name = fields.String()
    baseurl = fields.Nullable(fields.String())
    metalink = fields.Nullable(fields.String())
    mirrorlist = fields.Nullable(fields.String())
    enabled = fields.Boolean(default=True)
    additional_fields = fields.Nullable(fields.String())


class RepositoryFile(Model):
    topic = SystemFactsTopic

    file = fields.String()
    data = fields.List(fields.Model(RepositoryData))


class RepositoriesFacts(Model):
    topic = SystemFactsTopic

    repositories = fields.List(fields.Model(RepositoryFile))


@deprecated(
    since="2020-09-01",
    message=(
        "The model is temporary and not assumed to be used in any "
        "other actors."
    ),
)
class TMPTargetRepositoriesFacts(RepositoriesFacts):
    """Do not consume this model anywhere outside of localreposinhibit.

    The model is temporary and will be removed in close future
    """

    pass


# repos/system_upgrade/common/models/repositoriesmap.py

from leapp.models import Model, fields
from leapp.topics import TransactionTopic


class RepoMapEntry(Model):
    topic = TransactionTopic

    source = fields.String()
    """The source PES id."""

    target = fields.List(fields.String())
    """List of target PES ids"""


class PESIDRepositoryEntry(Model):
    """
    Represent metadata about particular repository.

    The metadata are used to identify purpose and nature of the repository.

    Warning: This model is not expected to be consumed/produced by any actor
    directly. As well, it is not covered by deprecation process and can be
    changed or removed any time.
    """

    topic = TransactionTopic

    pesid = fields.String()
    """
    The PES id of the repository.

    The PES id indicate family of YUM repositories. E.g. rhel8-BaseOS covers
    variants of BaseOS YUM repositories for all channels, architectures,
    RHUI, etc, which have basically same purpose.
    """

    major_version = fields.String()
    """
    The major version of OS.

    E.g. for RHEL 7.9 the major version is 7. Since we work with versions
    as with strings throughout the whole codebase, we keep this data type here too.
    """

    repoid = fields.String()
    """
    The repository ID which identifies the repository from YUM/DNF POV.
    """

    arch = fields.StringEnum(['x86_64', 's390x', 'ppc64le', 'aarch64'])
    """
    The architecture for which the repository is delivered.
    """

    repo_type = fields.StringEnum(['rpm', 'debug', 'srpm'])
    """
    The repository type.

    In our case, we usually map just repositories with the 'rpm' type, so
    usually you can see just this one, but the others are possible to add
    too.
    """

    channel = fields.StringEnum(['ga', 'tuv', 'e4s', 'eus', 'aus', 'beta'])
    """
    The 'channel' of the repository.

    The 'channel' could be a little bit inaccurate term, but let's use it in
    this project. The standard repositories has 'ga' channel. 'beta'
    repositories are unsupported for IPU, however they are useful for testing
    purposes. The other channels indicate premium repositories.
    """

    rhui = fields.StringEnum(['', 'aws', 'azure'])
    """
    Indicate whether the repository is deliver for RHUI and which one.

    For non-rhui systems: empty string
    For AWS or Azure: 'aws' / 'azure'
    """


class RepositoriesMapping(Model):
    """
    Private model containing information about mapping between repositories.

    Warning: We expect to be only consumers of this model.
    This means the model is not covered by deprecation process and can be
    changed or removed any time.
    """
    topic = TransactionTopic

    mapping = fields.List(fields.Model(RepoMapEntry), default=[])
    repositories = fields.List(fields.Model(PESIDRepositoryEntry), default=[])


# repos/system_upgrade/common/models/repositoriessetuptasks.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class RepositoriesSetupTasks(Model):
    """
    Information about repositories that must be managed in order to complete upgrade process.

    'to_enable' field consists of a list of repositories that should be enabled in order to complete
    upgrade process. This information should be processed by an actor dedicated to manage
    repositories.
    """
    topic = SystemFactsTopic

    to_enable = fields.List(fields.String(), default=[])


# repos/system_upgrade/common/models/rhsminfo.py

from leapp.models import Model, fields
from leapp.topics import RHSMTopic


class RHSMInfo(Model):
    """
    Subscription-manager details required for the inplace upgrade.
    """
    topic = RHSMTopic

    release = fields.Nullable(fields.String())
    """ Release the subscription-manager is set to. """
    attached_skus = fields.List(fields.String(), default=[])
    """ SKUs the current system is attached to. """
    available_repos = fields.List(fields.String(), default=[])
    """ Repositories that are available to the current system through the subscription-manager. """
    enabled_repos = fields.List(fields.String(), default=[])
    """ Repositories that are enabled on the current system through the subscription-manager. """
    existing_product_certificates = fields.List(fields.String(), default=[])
    """ Product certificates that are currently installed on the system. """
    sca_detected = fields.Boolean(default=False)
    """ Info about whether SCA manifest was used or not. """


# repos/system_upgrade/common/models/rhuiinfo.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class RHUIInfo(Model):
    """
    Facts about public cloud provider and RHUI infrastructure
    """
    topic = SystemInfoTopic

    provider = fields.String()
    """ Provider name """


# repos/system_upgrade/common/models/rootdirectory.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class RootSubdirectory(Model):
    """
    Representation of a single root subdirectory. Can be expanded as needed.
    """
    topic = SystemFactsTopic
    name = fields.String()
    target = fields.Nullable(fields.String())  # if it's a link


class RootDirectory(Model):
    topic = SystemFactsTopic
    items = fields.List(fields.Model(RootSubdirectory))  # should not be empty


# repos/system_upgrade/common/models/rpmtransactiontasks.py

from leapp.models import fields, Model, Module
from leapp.topics import TransactionTopic


class RpmTransactionTasks(Model):
    topic = TransactionTopic

    local_rpms = fields.List(fields.String(), default=[])
    to_install = fields.List(fields.String(), default=[])
    to_keep = fields.List(fields.String(), default=[])
    to_remove = fields.List(fields.String(), default=[])
    to_upgrade = fields.List(fields.String(), default=[])
    modules_to_enable = fields.List(fields.Model(Module), default=[])
    modules_to_reset = fields.List(fields.Model(Module), default=[])


class FilteredRpmTransactionTasks(RpmTransactionTasks):
    pass


class PESRpmTransactionTasks(RpmTransactionTasks):
    # Introduced because the framework struggles with solving dependency order of actors:
    # https://github.com/oamg/leapp/issues/491
    pass


# repos/system_upgrade/common/models/saphanainfo.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class SapHanaManifestEntry(Model):
    """ SAP HANA manifest file key/value pair entry """
    topic = SystemInfoTopic
    key = fields.String()
    """ Key of the key value pairs in a SAP HANA manifest file """
    value = fields.String()
    """ Value of the key value pairs in a SAP HANA manifest file """


class SapHanaInstanceInfo(Model):
    """ Details of SAP HANA instances """
    topic = SystemInfoTopic

    name = fields.String()
    """ Name of the instance """
    path = fields.String()
    """ Directory of the SAP HANA instance """
    instance_number = fields.String()
    """ SAP HANA Instance number """
    running = fields.Boolean()
    """ Is the instance currently running? """
    admin = fields.String()
    """ Name of the instance administrator """
    manifest = fields.List(fields.Model(SapHanaManifestEntry))
    """ Content of the SAP HANA manifest file """


class SapHanaInfo(Model):
    """
    This message contains information collected around SAP HANA

    If SAP HANA has been detected on the machine, it is running and which versions have been detected.
    """
    topic = SystemInfoTopic
    installed = fields.Boolean(default=False)
    """ True if SAP HANA has been detected on the system """
    running = fields.Boolean(default=False)
    """ True if an instance of SAP HANA is running """
    instances = fields.List(fields.Model(SapHanaInstanceInfo))
    """ List of instance details of SAP HANA detected on the system """


# repos/system_upgrade/common/models/selinux.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic, TransactionTopic


class SELinuxModule(Model):
    """SELinux module in cil including priority"""
    topic = SystemInfoTopic
    name = fields.String()
    priority = fields.Integer()
    content = fields.String()
    # lines removed due to content invalid on RHEL 8
    removed = fields.List(fields.String())


class SELinuxModules(Model):
    """
    List of selinux modules that are not part of distribution policy

    modules - list of custom policy modules (priority != 100,200)
    templates - List of installed udica templates
    """
    topic = SystemInfoTopic
    modules = fields.List(fields.Model(SELinuxModule))
    templates = fields.List(fields.Model(SELinuxModule))


class SELinuxCustom(Model):
    """SELinux customizations returned by semanage export"""
    topic = SystemInfoTopic
    commands = fields.List(fields.String())
    removed = fields.List(fields.String())


class SELinuxRequestRPMs(Model):
    """
    SELinux related RPM packages that need to be present after upgrade

    Listed packages provide types that where used in policy
    customizations (to_install), or the corresponding policy
    was installed on RHEL-7 installation with priority 200
    (to_keep).
    """
    topic = TransactionTopic
    to_keep = fields.List(fields.String(), default=[])
    to_install = fields.List(fields.String(), default=[])


# repos/system_upgrade/common/models/selinuxdecisions.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class SelinuxRelabelDecision(Model):
    topic = SystemInfoTopic

    set_relabel = fields.Boolean()


class SelinuxPermissiveDecision(Model):
    topic = SystemInfoTopic

    set_permissive = fields.Boolean()


# repos/system_upgrade/common/models/selinuxfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class SELinuxFacts(Model):
    topic = SystemFactsTopic

    runtime_mode = fields.Nullable(fields.StringEnum(['enforcing', 'permissive']))
    static_mode = fields.StringEnum(['enforcing', 'permissive', 'disabled'])
    enabled = fields.Boolean()
    policy = fields.StringEnum(['targeted', 'minimum', 'mls'])
    mls_enabled = fields.Boolean()


# repos/system_upgrade/common/models/skippedrepositories.py

from leapp.models import Model, fields
from leapp.topics import TransactionTopic


class SkippedRepositories(Model):
    """
    Message that contains all skipped repositories and the packages that will not be upgraded as a result of those
    repositories being skipped.
    """
    topic = TransactionTopic
    repos = fields.List(fields.String(), default=[])
    """ List of repositories ids that are going to be skipped for the upgrade """
    packages = fields.List(fields.String(), default=[])
    """ List of packages that are not going to be upgraded because of skipped repositories """


# repos/system_upgrade/common/models/storageinfo.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class PartitionEntry(Model):
    topic = SystemInfoTopic

    major = fields.String()
    minor = fields.String()
    blocks = fields.String()
    name = fields.String()


class FstabEntry(Model):
    topic = SystemInfoTopic

    fs_spec = fields.String()
    fs_file = fields.String()
    fs_vfstype = fields.String()
    fs_mntops = fields.String()
    fs_freq = fields.String()
    fs_passno = fields.String()


class MountEntry(Model):
    topic = SystemInfoTopic

    name = fields.String()
    mount = fields.String()
    tp = fields.String()
    options = fields.String()


class LsblkEntry(Model):
    topic = SystemInfoTopic

    name = fields.String()
    maj_min = fields.String()
    rm = fields.String()
    size = fields.String()
    ro = fields.String()
    tp = fields.String()
    mountpoint = fields.String()


class PvsEntry(Model):
    topic = SystemInfoTopic

    pv = fields.String()
    vg = fields.String()
    fmt = fields.String()
    attr = fields.String()
    psize = fields.String()
    pfree = fields.String()


class VgsEntry(Model):
    topic = SystemInfoTopic

    vg = fields.String()
    pv = fields.String()
    lv = fields.String()
    sn = fields.String()
    attr = fields.String()
    vsize = fields.String()
    vfree = fields.String()


class LvdisplayEntry(Model):
    topic = SystemInfoTopic

    lv = fields.String()
    vg = fields.String()
    attr = fields.String()
    lsize = fields.String()
    pool = fields.String()
    origin = fields.String()
    data = fields.String()
    meta = fields.String()
    move = fields.String()
    log = fields.String()
    cpy_sync = fields.String()
    convert = fields.String()


class SystemdMountEntry(Model):
    topic = SystemInfoTopic

    node = fields.String()
    path = fields.String()
    model = fields.String()
    wwn = fields.String()
    fs_type = fields.String()
    label = fields.String()
    uuid = fields.String()


class StorageInfo(Model):
    topic = SystemInfoTopic
    partitions = fields.List(fields.Model(PartitionEntry), default=[])
    fstab = fields.List(fields.Model(FstabEntry), default=[])
    mount = fields.List(fields.Model(MountEntry), default=[])
    lsblk = fields.List(fields.Model(LsblkEntry), default=[])
    pvs = fields.List(fields.Model(PvsEntry), default=[])
    vgs = fields.List(fields.Model(VgsEntry), default=[])
    lvdisplay = fields.List(fields.Model(LvdisplayEntry), default=[])
    systemdmount = fields.List(fields.Model(SystemdMountEntry), default=[])


# repos/system_upgrade/common/models/sysctlvariablesfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class SysctlVariable(Model):
    topic = SystemFactsTopic

    name = fields.String()
    value = fields.String()


class SysctlVariablesFacts(Model):
    topic = SystemFactsTopic

    sysctl_variables = fields.List(fields.Model(SysctlVariable))


# repos/system_upgrade/common/models/targetrepositories.py

from leapp.models import Model, fields
from leapp.topics import TransactionTopic


class TargetRepositoryBase(Model):
    topic = TransactionTopic
    repoid = fields.String()


class UsedTargetRepository(TargetRepositoryBase):
    pass


class RHELTargetRepository(TargetRepositoryBase):
    pass


class CustomTargetRepository(TargetRepositoryBase):
    name = fields.Nullable(fields.String())
    baseurl = fields.Nullable(fields.String())
    enabled = fields.Boolean(default=True)


class TargetRepositories(Model):
    topic = TransactionTopic
    rhel_repos = fields.List(fields.Model(RHELTargetRepository))
    custom_repos = fields.List(fields.Model(CustomTargetRepository), default=[])


class UsedTargetRepositories(Model):
    topic = TransactionTopic
    repos = fields.List(fields.Model(UsedTargetRepository))


class CustomTargetRepositoryFile(Model):
    topic = TransactionTopic
    file = fields.String()


# repos/system_upgrade/common/models/targetuserspace.py

from leapp.models import Model, fields
from leapp.topics import BootPrepTopic, TargetUserspaceTopic, TransactionTopic
from leapp.utils.deprecation import deprecated


class TargetUserSpaceInfo(Model):
    """
    Information about the target userspace container to be able to use it

    The target userspace container contains the most crucial part of the target
    system to be able to proceed the inplace upgrade process (let's simplify
    it, and call it bootstrap of the target system). It contains (e.g.) the
    package manager of the target system, so we can calculate and process
    the RPM transaction. Additionally, it is used to create the upgrade
    initramfs (see the UpgradeInitramfsTasks model for more information).

    See the TargetUserSpaceTasks model for possibilities to influence content
    of the container automatically.
    """
    topic = TransactionTopic

    path = fields.String()
    """
    Path to the created target userspace directory

    It could be used as a container. It contains top level rootfs
    directories (bin, usr, var, ...).
    """

    scratch = fields.String()
    """
    Path to the directory with stored xfs-ftype workaround files

    It's not possible to create overlayfs over XFS without the ftype attribute.
    To workaround this problem, we are creating these files with EXT4 FS
    inside.
    """

    mounts = fields.String()
    """
    Path to the directory with additional mountpoints for the target userspace
    container.

    E.g. the overlayfss of the host filesystems can be stored here.
    """


class CopyFile(Model):
    """
    Specify a file that should be copied from the host to the target userspace
    container
    """
    topic = TransactionTopic

    src = fields.String()
    """
    Cannonical path to the file (on the host) that should be copied
    """

    dst = fields.Nullable(fields.String())
    """
    The path inside the target userspace container where the file should
    be copied.

    Do not add the path to the container itself. E.g. when the file should be
    installed into /etc/myconf in the container context, set /etc/myconf,
    not /path/to/container/etc/myconf.

    If dst is not set, the path inside the container is same as on the host.
    """


class TargetUserSpacePreupgradeTasks(Model):
    """
    Influence content of the target userspace container

    See the TargetUserSpaceInfo model description for more info about the
    target userspace container.
    """
    topic = TransactionTopic

    copy_files = fields.List(fields.Model(CopyFile), default=[])
    """
    List of files on the host that should be copied into the container

    Directories are supported as well.

    If a file/dir already exists on the destination path, it is removed &
    replaced.
    """

    install_rpms = fields.List(fields.String(), default=[])
    """
    List of rpm names that are required to be installed when the container
    is created.
    """


class TargetUserSpaceUpgradeTasks(TargetUserSpacePreupgradeTasks):
    """
    Analogy to the TargetUserSpacePreupgradeTasks model, but focused
    on initramfs requirements.

    Generate this message to ensure all RPMs and configuration files, needed
    to be able to generate the upgrade initramfs (see UpgradeInitramfsTasks),
    are available inside the container.

    For performance reasons (do not download & install bunch of rpms before
    it's sure the upgrade is not inhibited) these tasks are executed just
    in time it's 'sure' the upgrade is going to happen.
    """


@deprecated(since='2021-10-10', message='Replaced by TargetUserSpacePreupgradeTasks.')
class RequiredTargetUserspacePackages(Model):
    topic = TargetUserspaceTopic
    packages = fields.List(fields.String(), default=[])


@deprecated(since='2021-10-10', message='Replaced by TargetUserSpaceInitrdEnvTasks')
class RequiredUpgradeInitramPackages(Model):
    """
    Requests packages to be installed so that the leapp upgrade dracut image generation will succeed
    """
    topic = BootPrepTopic

    packages = fields.List(fields.String(), default=[])
    """
    List of packages names to install on the target userspace so their content can be included in the initram disk
    """


# repos/system_upgrade/common/models/transactioncompleted.py

from leapp.models import Model
from leapp.topics import TransactionTopic


class TransactionCompleted(Model):
    topic = TransactionTopic


class TransactionDryRun(Model):
    topic = TransactionTopic


# repos/system_upgrade/common/models/udev.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class UdevAdmInfoData(Model):
    topic = SystemInfoTopic

    db = fields.String()
    """Database export obtained by executing 'udevadm info -e'."""


# repos/system_upgrade/common/models/usedrepositories.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class UsedRepository(Model):
    """
    Describe list of current packages installed from a specific repository
    """

    topic = SystemInfoTopic

    repository = fields.String()
    packages = fields.List(fields.String(), default=[])


class UsedRepositories(Model):
    """
    Describe list of used repositories in the current system
    """

    topic = SystemInfoTopic

    repositories = fields.List(fields.Model(UsedRepository), default=[])


# repos/system_upgrade/common/models/usersfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class User(Model):
    topic = SystemFactsTopic

    name = fields.String()
    uid = fields.Integer()
    gid = fields.Integer()
    home = fields.String()


class UsersFacts(Model):
    topic = SystemFactsTopic

    users = fields.List(fields.Model(User))


# repos/system_upgrade/common/models/xfspresence.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class XFSPresence(Model):
    """
    A generic message reporting mountpoints with XFS using ftype = 0
    """
    topic = SystemInfoTopic

    present = fields.Boolean(default=False)
    """ XFS is used on at least one mountpoint """

    without_ftype = fields.Boolean(default=False)
    """ At least one mountpoint is using XFS with ftype=0 """

    mountpoints_without_ftype = fields.List(fields.String(), default=[])
    """ List of mountpoints that have ftype=0 """


# repos/system_upgrade/common/models/yumconfig.py

from leapp.models import fields, Model
from leapp.topics import SystemFactsTopic


class YumConfig(Model):
    topic = SystemFactsTopic

    enabled_plugins = fields.List(fields.String(), default=[])


# repos/system_upgrade/common/tags/applications.py

from leapp.tags import Tag


class ApplicationsPhaseTag(Tag):
    name = 'applications'


# repos/system_upgrade/common/tags/checks.py

from leapp.tags import Tag


class ChecksPhaseTag(Tag):
    name = 'checks'


class TargetTransactionChecksPhaseTag(Tag):
    name = 'target_checks'


# repos/system_upgrade/common/tags/download.py

from leapp.tags import Tag


class DownloadPhaseTag(Tag):
    name = 'download'


# repos/system_upgrade/common/tags/facts.py

from leapp.tags import Tag


class FactsPhaseTag(Tag):
    name = 'facts'


class TargetTransactionFactsPhaseTag(Tag):
    name = 'target_transaction_facts'


# repos/system_upgrade/common/tags/finalization.py

from leapp.tags import Tag


class FinalizationPhaseTag(Tag):
    name = 'finalization'


# repos/system_upgrade/common/tags/firstboot.py

from leapp.tags import Tag


class FirstBootPhaseTag(Tag):
    name = 'first_boot'


# repos/system_upgrade/common/tags/initramstart.py

from leapp.tags import Tag


class InitRamStartPhaseTag(Tag):
    name = 'init_ram_start'


# repos/system_upgrade/common/tags/interimpreparation.py

from leapp.tags import Tag


class InterimPreparationPhaseTag(Tag):
    name = 'interim_preparation'


# repos/system_upgrade/common/tags/ipu.py

from leapp.tags import Tag


class IPUWorkflowTag(Tag):
    name = 'ipu'


# repos/system_upgrade/common/tags/latetests.py

from leapp.tags import Tag


class LateTestsPhaseTag(Tag):
    name = 'late_tests'


# repos/system_upgrade/common/tags/preparation.py

from leapp.tags import Tag


class PreparationPhaseTag(Tag):
    name = 'preparation'


# repos/system_upgrade/common/tags/report.py

from leapp.tags import Tag


class ReportPhaseTag(Tag):
    name = 'report'


# repos/system_upgrade/common/tags/rpmupgrade.py

from leapp.tags import Tag


class RPMUpgradePhaseTag(Tag):
    name = 'rpm_upgrade'


# repos/system_upgrade/common/tags/thirdpartyapplications.py

from leapp.tags import Tag


class ThirdPartyApplicationsPhaseTag(Tag):
    name = 'third_party_applications'


# repos/system_upgrade/common/topics/bootprep.py

from leapp.topics import Topic


class BootPrepTopic(Topic):
    name = 'boot_prep'


# repos/system_upgrade/common/topics/rhsm.py

from leapp.topics import Topic


class RHSMTopic(Topic):
    name = 'rhsm'


# repos/system_upgrade/common/topics/sctpconfigtopic.py

from leapp.topics import Topic


class SCTPConfigTopic(Topic):
    name = 'sctp_config_topic'


# repos/system_upgrade/common/topics/systemfacts.py

from leapp.topics import Topic


class SystemFactsTopic(Topic):
    name = 'system_facts'


# repos/system_upgrade/common/topics/targetuserspace.py

from leapp.topics import Topic


class TargetUserspaceTopic(Topic):
    name = 'target_userspace'


# repos/system_upgrade/common/topics/transaction.py

from leapp.topics import Topic


class TransactionTopic(Topic):
    name = 'transaction'


# repos/system_upgrade/common/workflows/inplace_upgrade.py

from leapp import tags
from leapp.models import IPUConfig
from leapp.workflows import Workflow
from leapp.workflows.flags import Flags
from leapp.workflows.phases import Phase
from leapp.workflows.policies import Policies
from leapp.workflows.tagfilters import TagFilter


class IPUWorkflow(Workflow):
    """In-Place Upgrade workflow used by the leapp utility to process the in-place upgrade."""

    name = 'InplaceUpgrade'
    tag = tags.IPUWorkflowTag
    short_name = 'ipu'
    configuration = IPUConfig
    description = """The IPU workflow takes care of an in-place upgrade (IPU) of RHEL 7 to RHEL 8."""

    class FactsCollectionPhase(Phase):
        """
        Get information (facts) about the system (e.g. installed packages, configuration, ...).

        No decision should be done in this phase. Scan the system to get information you need and provide
        it to other actors in the following phases.
        """

        name = 'FactsCollection'
        filter = TagFilter(tags.FactsPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class ChecksPhase(Phase):
        """
        Check upgradability of the system, produce user question if needed and produce output for the report.

        Check whether it is possible to upgrade the system and detect potential risks. It is not expected to get
        additional information about the system in this phase, but rather work with data provided by the actors from
        the FactsCollection. When a potential risk is detected for upgrade, produce messages for the Reports phase.
        """

        name = 'Checks'
        filter = TagFilter(tags.ChecksPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class TargetTransactionFactsCollectionPhase(Phase):
        """
        Get information about target system. Analogy of FactsCollectionPhase for target system.

        Here we can collect information what repositories are available on target system,
        what is expected calculation of target transaction (what will be instaled, removed, ...
        """

        name = 'TargetTransactionFactsCollection'
        filter = TagFilter(tags.TargetTransactionFactsPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class TargetTransactionChecksPhase(Phase):
        """
        Checks upgradability regarding the information gathered about the target system.

        Check whether expected repositories and rpms are available, what rpms are planned
        to install, remove, ...

        IOW, checks related to rpm transaction mainly.
        """

        name = 'TargetTransactionCheck'
        filter = TagFilter(tags.TargetTransactionChecksPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class ReportsPhase(Phase):
        """Provide user with the result of the checks."""

        name = 'Reports'
        filter = TagFilter(tags.ReportPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    # The following two phases have been removed but are kept here for reference or in case they are needed again

    # class AttachPackageReposPhase(Phase):
    #    name = 'AttachPackageRepos'
    #    #NOTE: in case of use the AttachPackageReposPhaseTag tag has to be created
    #    filter = TagFilter(tags.AttachPackageReposPhaseTag)
    #    policies = Policies(Policies.Errors.FailPhase,
    #                        Policies.Retry.Phase)
    #    flags = Flags()

    # class PlanningPhase(Phase):
    #    name = 'Planning'
    #    #NOTE: in case of use the PlanningPhaseTag tag has to be created
    #    filter = TagFilter(tags.PlanningPhaseTag)
    #    policies = Policies(Policies.Errors.FailPhase,
    #                        Policies.Retry.Phase)
    #    flags = Flags()

    class DownloadPhase(Phase):
        """Download data needed for the upgrade and prepare RPM transaction for the upgrade."""

        name = 'Download'
        filter = TagFilter(tags.DownloadPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class InterimPreparationPhase(Phase):
        """Prepare an initial RAM file system (if required). Setup bootloader."""

        name = 'InterimPreparation'
        filter = TagFilter(tags.InterimPreparationPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags(request_restart_after_phase=True)

    class InitRamStartPhase(Phase):
        """Boot into the upgrade initramfs, mount disks, etc."""

        name = 'InitRamStart'
        filter = TagFilter(tags.InitRamStartPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    # The following two phases have been removed but are kept here for reference or in case they are needed again

    # class NetworkPhase(Phase):
    #    name = 'Network'
    #    #NOTE: in case of use the NetworkPhaseTag tag has to be created
    #    filter = TagFilter(tags.NetworkPhaseTag)
    #    policies = Policies(Policies.Errors.FailPhase,
    #                        Policies.Retry.Phase)
    #    flags = Flags()

    # class StoragePhase(Phase):
    #    name = 'Storage'
    #    #NOTE: in case of use the StoragePhaseTag tag has to be created
    #    filter = TagFilter(tags.StoragePhaseTag)
    #    policies = Policies(Policies.Errors.FailPhase,
    #                        Policies.Retry.Phase)
    #    flags = Flags()

    class LateTestsPhase(Phase):
        """Last tests before the RPM upgrade that have to be done with the new kernel and systemd."""

        name = 'LateTests'
        filter = TagFilter(tags.LateTestsPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class PreparationPhase(Phase):
        """Prepare the environment to ascertain success of the RPM upgrade transaction."""

        name = 'Preparation'
        filter = TagFilter(tags.PreparationPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class RPMUpgradePhase(Phase):
        """Perform the RPM transaction, i.e. upgrade the RPMs."""

        name = 'RPMUpgrade'
        filter = TagFilter(tags.RPMUpgradePhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags(is_checkpoint=True)

    class ApplicationsPhase(Phase):
        """
        Perform the neccessary steps to finish upgrade of applications provided by Red Hat.

        This may include moving/renaming of configuration files, modifying configuration of applications to be able
        to run correctly and with as similar behaviour to the original as possible.
        """

        name = 'Applications'
        filter = TagFilter(tags.ApplicationsPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class ThirdPartyApplicationsPhase(Phase):
        """Analogy to the Applications phase, but for third party and custom applications."""

        name = 'ThirdPartyApplications'
        filter = TagFilter(tags.ThirdPartyApplicationsPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()

    class FinalizationPhase(Phase):
        """
        Additional actions that should be done before rebooting into the upgraded system.

        For example SELinux relabeling.
        """

        name = 'Finalization'
        filter = TagFilter(tags.FinalizationPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags(restart_after_phase=True)

    class FirstBootPhase(Phase):
        """Actions to be done right after booting into the upgraded system."""

        name = 'FirstBoot'
        filter = TagFilter(tags.FirstBootPhaseTag)
        policies = Policies(Policies.Errors.FailPhase,
                            Policies.Retry.Phase)
        flags = Flags()


# repos/system_upgrade/el8toel9/models/firewalldglobalconfig.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class FirewalldGlobalConfig(Model):
    """
    The model contains firewalld global configuration. The configuration is
    usually located at /etc/firewalld/firewalld.conf.
    """
    topic = SystemInfoTopic

    # Defaults for RHEL-9.
    #
    defaultzone = fields.String(default='public')
    cleanuponexit = fields.Boolean(default=True)
    cleanupmodulesonexit = fields.Boolean(default=False)
    lockdown = fields.Boolean(default=False)
    ipv6_rpfilter = fields.Boolean(default=True)
    individualcalls = fields.Boolean(default=False)
    logdenied = fields.String(default='off')
    firewallbackend = fields.String(default='nftables')
    flushallonreload = fields.Boolean(default=True)
    rfc3964_ipv4 = fields.Boolean(default=True)

    # These have been removed in RHEL-9.
    #
    allowzonedrifting = fields.Boolean(default=False)


# repos/system_upgrade/el8toel9/models/firewalldusedobjectnames.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class FirewalldUsedObjectNames(Model):
    """
    This model contains lists of firewalld object (e.g. zones, services) names
    in use by the permanent firewalld configuration.
    """
    topic = SystemInfoTopic

    services = fields.List(fields.String(), default=[])
    """
    list of services (names) in use by firewalld's permanent configuration

    e.g. ["ssh", "https"]
    """

    policies = fields.List(fields.String(), default=[])
    """
    list of policies (names) in use by firewalld's permanent configuration

    e.g. ["allow-host-ipv6", "mypolicy"]
    """

    zones = fields.List(fields.String(), default=[])
    """
    list of zones (names) in use by firewalld's permanent configuration

    e.g. ["public", "internal", "nm-shared", "libvirt"]
    """


# repos/system_upgrade/el8toel9/models/sssd.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class SSSDConfig8to9(Model):
    """
    SSSD configuration that is related to the upgrade process.
    """
    topic = SystemInfoTopic

    enable_files_domain_set = fields.Boolean()
    """
    True if [sssd]/enable_files_domain is explicitly set.
    """

    explicit_files_domain = fields.Boolean()
    """
    True if a domain with id_provider=files exist.
    """

    pam_cert_auth = fields.Boolean()
    """
    True if [pam]/pam_cert_auth is set to True.
    """


# repos/system_upgrade/el8toel9/models/vdoconversioninfo.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class VdoConversionDevice(Model):
    topic = SystemInfoTopic
    name = fields.String()


class VdoConversionPreDevice(VdoConversionDevice):
    pass


class VdoConversionFailiableDevice(VdoConversionDevice):
    # `check_failed` and `failure` are only set if the checking process fails.
    check_failed = fields.Boolean(default=False)
    failure = fields.Nullable(fields.String())


class VdoConversionPostDevice(VdoConversionFailiableDevice):
    complete = fields.Boolean()


class VdoConversionUndeterminedDevice(VdoConversionFailiableDevice):
    # There are only devices which are undetermined as to VDO conversion if
    # lvm is installed on the system and either vdo is not or
    # `vdoprepareforlvm` fails.
    #
    # If the vdo package is not installed on the system `check_failed` is
    # false (the device wasn't actually checked); this indicates the absence of
    # the vdo package on the system.
    pass


class VdoConversionInfo(Model):
    # If lvm is not installed on the system there can be no VDO instances.
    # In that case VdoConversionInfo is generated with empty lists of devices.
    topic = SystemInfoTopic

    pre_conversion = fields.List(fields.Model(VdoConversionPreDevice), default=[])
    post_conversion = fields.List(fields.Model(VdoConversionPostDevice), default=[])
    undetermined_conversion = fields.List(fields.Model(VdoConversionUndeterminedDevice), default=[])


# conftest.py

import logging
import os

from leapp.repository.manager import RepositoryManager
from leapp.repository.scan import find_and_scan_repositories
from leapp.utils.repository import find_repository_basedir, get_repository_id

logger = logging.getLogger(__name__)
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("parso").setLevel(logging.INFO)


def _load_and_add_repo(manager, repo_path):
    repo = find_and_scan_repositories(
        repo_path,
        include_locals=True
    )
    unloaded = set()
    loaded = {r.repo_id for r in manager.repos}
    if hasattr(repo, 'repos'):
        for repo in repo.repos:
            if not manager.repo_by_id(repo.repo_id):
                manager.add_repo(repo)
                unloaded.add(repo.repo_id)
    else:
        manager.add_repo(repo)
    if not loaded:
        manager.load(skip_actors_discovery=True)
    else:
        for repo_id in unloaded:
            manager.repo_by_id(repo_id).load(skip_actors_discovery=True)


def pytest_collectstart(collector):
    if collector.nodeid:
        current_repo_basedir = find_repository_basedir(str(collector.fspath))
        if not current_repo_basedir:
            # This is not a repository
            return
        if not hasattr(collector.session, "leapp_repository"):
            collector.session.leapp_repository = RepositoryManager()
            collector.session.repo_base_dir = current_repo_basedir
            _load_and_add_repo(collector.session.leapp_repository, current_repo_basedir)
        else:
            if not collector.session.leapp_repository.repo_by_id(
                get_repository_id(current_repo_basedir)
            ):
                _load_and_add_repo(collector.session.leapp_repository, current_repo_basedir)

        # we're forcing the actor context switch only when traversing new
        # actor
        if "/actors/" in str(collector.fspath) and (
            not hasattr(collector.session, "current_actor_path")
            or collector.session.current_actor_path + os.sep
            not in str(collector.fspath)
        ):
            actor = None
            for a in collector.session.leapp_repository.actors:
                if a.full_path == collector.fspath.dirpath().dirname:
                    actor = a
                    break

            if not actor:
                logger.info("No actor found, exiting collection...")
                return
            # we need to tear down the context from the previous
            # actor
            try:
                collector.session.current_actor_context.__exit__(
                    None, None, None
                )
            except AttributeError:
                pass
            else:
                logger.info(
                    "Actor %r context teardown complete",
                    collector.session.current_actor.name,
                )

            logger.info("Injecting actor context for %r", actor.name)
            collector.session.current_actor = actor
            collector.session.current_actor_context = actor.injected_context()
            collector.session.current_actor_context.__enter__()
            collector.session.current_actor_path = (
                collector.session.current_actor.full_path
            )
            logger.info("Actor %r context injected", actor.name)


def pytest_runtestloop(session):
    try:
        session.current_actor_context.__exit__(None, None, None)
        logger.info(
            "Actor %r context teardown complete", session.current_actor.name,
        )
    except AttributeError:
        pass


# repos/system_upgrade/el7toel8/actors/authselectapply/actor.py

from leapp.actors import Actor
from leapp.libraries.stdlib import run, CalledProcessError
from leapp.models import Authselect, AuthselectDecision
from leapp import reporting
from leapp.reporting import Report, create_report
from leapp.tags import IPUWorkflowTag, ApplicationsPhaseTag


resources = [
    reporting.RelatedResource('package', 'authselect'),
    reporting.RelatedResource('package', 'authconfig'),
    reporting.RelatedResource('file', '/etc/nsswitch.conf')
]


class AuthselectApply(Actor):
    """
    Apply changes suggested by AuthselectScanner.

    If confirmed by admin in AuthselectDecision, call suggested authselect
    command to configure the system using this tool.
    """

    name = 'authselect_apply'
    consumes = (Authselect, AuthselectDecision,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ApplicationsPhaseTag)

    def process(self):
        model = next(self.consume(Authselect))
        decision = next(self.consume(AuthselectDecision))

        if not decision.confirmed or model.profile is None:
            return

        command = ['authselect', 'select', '--force', model.profile] + model.features

        try:
            run(command)
        except CalledProcessError as err:
            create_report([  # pylint: disable-msg=too-many-arguments
                reporting.Title('Authselect call failed'),
                reporting.Summary(str(err)),
                reporting.Severity(reporting.Severity.MEDIUM),
                reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.TOOLS
                ]),
                reporting.Flags([
                    reporting.Flags.FAILURE
                ])
            ] + resources)  # pylint: disable-msg=too-many-arguments
            return

        try:
            run(['systemctl', 'enable', 'oddjobd.service'])
        except (OSError, CalledProcessError) as e:
            self.log.warning('Error enabling oddjobd.service: {}'.format(e))

        create_report([  # pylint: disable-msg=too-many-arguments
            reporting.Title('System was converted to authselect.'),
            reporting.Summary(
                'System was converted to authselect with the '
                'following call: "{}"'.format(' '.join(command))
            ),
            reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.TOOLS
                ])
        ] + resources)  # pylint: disable-msg=too-many-arguments


# repos/system_upgrade/el7toel8/actors/authselectcheck/actor.py

from leapp.actors import Actor
from leapp.dialogs import Dialog
from leapp.dialogs.components import BooleanComponent
from leapp.models import Authselect, AuthselectDecision
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import IPUWorkflowTag, ChecksPhaseTag


resources = [
    reporting.RelatedResource('package', 'authselect'),
    reporting.RelatedResource('package', 'authconfig'),
    reporting.RelatedResource('file', '/etc/nsswitch.conf')
]


class AuthselectCheck(Actor):
    """
    Confirm suggested authselect call from AuthselectScanner.

    AuthselectScanner produces an Authselect model that contains changes
    that are suggested based on current configuration. This actor will
    ask administrator for confirmation and will report the result.
    """

    name = 'authselect_check'
    consumes = (Authselect,)
    produces = (AuthselectDecision, Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)
    dialogs = (
        Dialog(
            scope='authselect_check',
            reason='Confirmation',
            components=(
                BooleanComponent(
                    key='confirm',
                    label='Configure PAM and nsswitch.conf with the following '
                          'authselect call?',
                    default=True,
                    description='If yes, suggested authselect profile will '
                                'be applied on your system to generate '
                                'PAM and nsswitch.conf configuration. '
                                'If no, current configuration will be kept '
                                'intact.',
                    reason='There is a new tool called authselect in RHEL8 '
                           'that replaced authconfig which is used to manage '
                           'authentication (PAM) and identity (nsswitch.conf) '
                           'sources. It is recommended to switch to this tool.'
                ),
            )
        ),
    )

    def process(self):
        model = next(self.consume(Authselect))

        # If there is no equivalent authselect profile we will not touch
        # the current configuration. Therefore there is no need for
        # confirmation.
        if model.profile is None:
            self.produce_current_configuration(model)
            return

        command = 'authselect select {0} {1} --force'.format(
            model.profile,
            ' '.join(model.features)
        )

        # We do not need admin confirmation if the current
        # configuration was generated with authconfig.
        if not model.confirm:
            self.produce_authconfig_configuration(model, command)
            return

        # Authselect profile is available but we require confirmation.
        confirmed = self.get_confirmation(model, command)
        if confirmed is not None:
            # A user has made his choice
            self.produce_suggested_configuration(model, confirmed, command)

    def get_confirmation(self, model, command):
        dialog = self.dialogs[0]

        dialog.components[0].label += " {}".format(command)

        return self.get_answers(dialog).get('confirm')

    def produce_authconfig_configuration(self, model, command):
        self.produce(
            AuthselectDecision(
                confirmed=True
            )
        )

        create_report([
            reporting.Title(
                'Authselect will be used to configure PAM and nsswitch.conf.'
            ),
            reporting.Summary(
                'There is a new tool called authselect in RHEL8 that '
                'replaced authconfig. The upgrade process detected '
                'that authconfig was used to generate current '
                'configuration and it will automatically convert it '
                'to authselect. Authselect call is: {}. The process will '
                'also enable "oddjobd" systemd service on startup'.format(command)
            ),
            reporting.Tags([
                reporting.Tags.AUTHENTICATION,
                reporting.Tags.SECURITY,
                reporting.Tags.TOOLS
            ])
        ] + resources)

    def produce_current_configuration(self, model):
        self.produce(
            AuthselectDecision(
                confirmed=False
            )
        )

        create_report([
            reporting.Title(
                'Current PAM and nsswitch.conf configuration will be kept.'
            ),
            reporting.Summary(
                'There is a new tool called authselect in RHEL8 that '
                'replaced authconfig. The upgrade process was unable '
                'to find an authselect profile that would be equivalent '
                'to your current configuration. Therefore your '
                'configuration will be left intact.'
            ),
            reporting.Tags([
                reporting.Tags.AUTHENTICATION,
                reporting.Tags.SECURITY,
                reporting.Tags.TOOLS
            ]),
            reporting.Severity(reporting.Severity.INFO)
        ] + resources)

    def produce_suggested_configuration(self, model, confirmed, command):
        self.produce(
            AuthselectDecision(
                confirmed=confirmed
            )
        )
        if confirmed:
            create_report([
                reporting.Title(
                    'Authselect will be used to configure PAM and nsswitch.conf.'
                ),
                reporting.Summary(
                    'There is a new tool called authselect in RHEL8 that '
                    'replaced authconfig. The upgrade process suggested '
                    'an authselect profile that is similar to your '
                    'current configuration and your system will be switched '
                    'to this profile. Authselect call is: {}. The process will '
                    'also enable "oddjobd" systemd service on startup'.format(command)
                ),
                reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.TOOLS
                ])
            ] + resources)

        else:
            create_report([
                reporting.Title(
                    'Current PAM and nsswitch.conf configuration will be kept.'
                ),
                reporting.Summary(
                    'There is a new tool called authselect in RHEL8 that '
                    'replaced authconfig. The upgrade process suggested '
                    'an authselect profile that is similar to your '
                    'current configuration. However this suggestion was '
                    'refused therefore existing configuration will be kept '
                    'intact.',
                ),
                reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.TOOLS
                ]),
                reporting.Remediation(commands=[[command]]),
                reporting.Severity(reporting.Severity.MEDIUM)
            ] + resources)


# repos/system_upgrade/el7toel8/actors/authselectscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.authselectscanner import (
    Authconfig,
    AuthselectScannerLibrary,
    DConf,
    read_file,
)
from leapp.libraries.common.pam import PAM
from leapp.models import Authselect
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class AuthselectScanner(Actor):
    """
    Detect what authselect configuration should be suggested to administrator.

    1. Detect possible authselect profile by looking up modules in PAM
       or by checking that daemon is enabled.
       - pam_sss -> sssd
       - pam_winbind -> winbind
       - ypbind enabled -> nis

       If more then one module/daemon is detected that we will keep the
       configuration intact. No authselect profile can be applied.

    2. Detect authselect profile features by looking up modules in PAM
       or nsswitch.conf.
       - pam_faillock => with-faillock
       - pam_fprintd => with-fingerprint
       - pam_access => with-pamaccess
       - pam_mkhomedir => with-mkhomedir
       - pam_oddjob_mkhomedir => with-mkhomedir

    3. Check if there are any unknown PAM modules.
       If there are used PAM modules not used in authselect (such as pam_ldap),
       we must keep the configuration intact.

    4. Check if authconfig was used to create current configuration.
       If yes, we can automatically convert the configuration to authselect.
       If no, we need admin's confirmation.

       - Check that /etc/sysconfig/authconfig exists.
       - Check that PAM configuration uses authconfig files.
       - Check that PAM configuration was not touch after sysconfig file
         was created.
    """

    name = 'authselect_scanner'
    consumes = ()
    produces = (Authselect,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    known_modules = [
        'pam_access',
        'pam_deny',
        'pam_ecryptfs',
        'pam_env',
        'pam_faildelay',
        'pam_faillock',
        'pam_fprintd',
        'pam_keyinit',
        'pam_krb5',
        'pam_lastlog',
        'pam_limits',
        'pam_localuser',
        'pam_mkhomedir',
        'pam_oddjob_mkhomedir',
        'pam_permit',
        'pam_pkcs11',
        'pam_pwquality',
        'pam_sss',
        'pam_succeed_if',
        'pam_systemd',
        'pam_u2f',
        'pam_umask',
        'pam_unix',
        'pam_winbind'
    ]
    """
    List of PAM modules that are known by authselect.
    """

    def process(self):
        # Load configuration
        ac = Authconfig(read_file('/etc/sysconfig/authconfig'))
        dconf = DConf(read_file('/etc/dconf/db/distro.d/10-authconfig'))
        pam = PAM.from_system_configuration()
        nsswitch = read_file("/etc/nsswitch.conf")

        scanner = AuthselectScannerLibrary(
            self.known_modules,
            ac, dconf, pam, nsswitch
        )

        self.produce(scanner.process())


# repos/system_upgrade/el7toel8/actors/checkacpid/actor.py

from leapp.actors import Actor
from leapp.models import InstalledRedHatSignedRPM
from leapp.libraries.common.rpms import has_package
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckAcpid(Actor):
    """
    Check if acpid is installed. If yes, write information about non-compatible changes.
    """

    name = 'checkacpid'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'acpid'):
            create_report([
                reporting.Title('Acpid incompatible changes in the next major version'),
                reporting.Summary('The option -d (debug) no longer implies -f (foreground).'),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Remediation(
                    hint='You must now use both options (\'-df\') for the same behavior. Please update '
                         'your scripts to be compatible with the changes.'),
                reporting.Tags([reporting.Tags.KERNEL, reporting.Tags.SERVICES]),
                reporting.RelatedResource('package', 'acpid')
            ])


# repos/system_upgrade/el7toel8/actors/checkbrltty/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkbrltty
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM, BrlttyMigrationDecision
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag

related = [reporting.RelatedResource('package', 'brltty')]


class CheckBrltty(Actor):
    """
    Check if brltty is installed, check whether configuration update is needed.
    """

    name = 'check_brltty'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report, BrlttyMigrationDecision,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'brltty'):
            create_report([
                reporting.Title('Brltty has incompatible changes in the next major version'),
                reporting.Summary(
                    'The --message-delay brltty option has been renamed to --message-timeout.\n'
                    'The -U [--update-interval=] brltty option has been removed.'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags([reporting.Tags.ACCESSIBILITY]),
                reporting.Remediation(
                    hint='Please update your scripts to be compatible with the changes.'
                )
            ] + related)

            (migrate_file, migrate_bt, migrate_espeak,) = checkbrltty.check_for_unsupported_cfg()
            report_summary = ''
            if migrate_bt:
                report_summary = 'Unsupported aliases for bluetooth devices (\'bth:\' and \'bluez:\') will be '
                report_summary += 'renamed to \'bluetooth:\'.'
            if migrate_espeak:
                if report_summary:
                    report_summary += '\n'
                report_summary += 'eSpeak speech driver is no longer supported, it will be switched to eSpeak-NG.'
            if report_summary:
                create_report([
                    reporting.Title('brltty configuration will be migrated'),
                    reporting.Summary(report_summary),
                    reporting.Severity(reporting.Severity.LOW),
                    reporting.Tags([reporting.Tags.ACCESSIBILITY]),
                ] + related)

                self.produce(BrlttyMigrationDecision(migrate_file=migrate_file, migrate_bt=migrate_bt,
                                                     migrate_espeak=migrate_espeak))


# repos/system_upgrade/el7toel8/actors/checkbtrfs/actor.py

from leapp.actors import Actor
from leapp.models import ActiveKernelModulesFacts
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp import reporting
from leapp.reporting import Report, create_report


class CheckBtrfs(Actor):
    """
    Check if Btrfs filesystem is in use. If yes, inhibit the upgrade process.

    Btrfs filesystem was introduced as Technology Preview with initial releases of RHEL 6 and 7. It
    was deprecated on versions 6.6 and 7.4 and will not be present in next major version.
    """

    name = 'check_btrfs'
    consumes = (ActiveKernelModulesFacts,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):

        hint = 'In order to unload the module from the running system, check the accompanied command.'
        command = ['modprobe', '-r', 'btrfs']

        for fact in self.consume(ActiveKernelModulesFacts):
            for active_module in fact.kernel_modules:
                if active_module.filename == 'btrfs':
                    create_report([
                        reporting.Title('Btrfs has been removed from RHEL8'),
                        reporting.Summary(
                            'The Btrfs file system was introduced as Technology Preview with the '
                            'initial release of Red Hat Enterprise Linux 6 and Red Hat Enterprise Linux 7. As of '
                            'versions 6.6 and 7.4 this technology has been deprecated and removed in RHEL8.'
                        ),
                        reporting.ExternalLink(
                            title='Considerations in adopting RHEL 8 - btrfs has been removed.',
                            url='https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/considerations_in_adopting_rhel_8/file-systems-and-storage_considerations-in-adopting-rhel-8#btrfs-has-been-removed_file-systems-and-storage'  # noqa: E501; pylint: disable=line-too-long
                        ),
                        reporting.ExternalLink(
                            title='How do I prevent a kernel module from loading automatically?',
                            url='https://access.redhat.com/solutions/41278'
                        ),
                        reporting.Severity(reporting.Severity.HIGH),
                        reporting.Flags([reporting.Flags.INHIBITOR]),
                        reporting.Tags([reporting.Tags.FILESYSTEM]),
                        reporting.Remediation(hint=hint, commands=[command]),
                        reporting.RelatedResource('kernel-driver', 'btrfs')
                    ])
                    break


# repos/system_upgrade/el7toel8/actors/checkchrony/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkchrony import check_chrony
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckChrony(Actor):
    """
    Check for incompatible changes in chrony configuration.

    Warn that the default chrony configuration in RHEL8 uses the leapsectz
    directive.
    """

    name = 'check_chrony'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        check_chrony(has_package(InstalledRedHatSignedRPM, 'chrony'))


# repos/system_upgrade/el7toel8/actors/checkcifs/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkcifs import checkcifs
from leapp.models import StorageInfo
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckCIFS(Actor):
    """
    Check if CIFS filesystem is in use. If yes, inhibit the upgrade process.

    Actor looks for CIFS in /ets/fstab.
    If there is a CIFS entry, the upgrade is inhibited.
    """
    name = "check_cifs"
    consumes = (StorageInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag,)

    def process(self):
        checkcifs(self.consume(StorageInfo))


# repos/system_upgrade/el7toel8/actors/checkcpu/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import cpu
from leapp.models import CPUInfo, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckCPU(Actor):
    """
    Check whether the CPU is supported by the target system. Inhibit upgrade if not.

    Currently we know just about cases with s390x where the set of CPUs supported
    by RHEL 8 is subset of CPUs supported on RHEL 7. We can detect such cases based
    on the machine field inside the /proc/cpuinfo file. expected values of the
    field on supported machines are: 2964, 2965, 3906, 3907, 8561, 8562.
    """

    name = "checkcpu"
    consumes = (CPUInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag,)

    def process(self):
        cpu.process()


# repos/system_upgrade/el7toel8/actors/checkdosfstools/actor.py

from leapp.actors import Actor
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckDosfstools(Actor):
    """
    Check if dosfstools is installed. If yes, write information about non-compatible changes.
    """

    name = 'checkdosfstools'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'dosfstools'):
            create_report([
                reporting.Title('Dosfstools incompatible changes in the next major version'),
                reporting.Summary(
                    'The automatic alignment of data clusters that was added in 3.0.8 and broken for '
                    'FAT32 starting with 3.0.20 has been reinstated. If you need to create file systems '
                    'for finicky devices that have broken FAT implementations use the option -a to '
                    'disable alignment.\n'
                    'The fsck.fat now defaults to interactive repair mode which previously had to be '
                    'selected with the -r option.\n'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags([
                        reporting.Tags.FILESYSTEM,
                        reporting.Tags.TOOLS
                ]),
                reporting.Remediation(hint='Please update your scripts to be compatible with the changes.'),
                reporting.RelatedResource('package', 'dosfstools')
            ])


# repos/system_upgrade/el7toel8/actors/checkfirewalld/actor.py

from leapp.actors import Actor
from leapp.models import FirewalldFacts
from leapp.libraries.actor import private
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


related = [reporting.RelatedResource('package', 'firewalld')]


class CheckFirewalld(Actor):
    """
    Check for certain firewalld configuration that may prevent an upgrade.
    """

    name = 'check_firewalld'
    consumes = (FirewalldFacts,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        unsupported_tables = []
        unsupported_ipset_types = []
        list_separator_fmt = '\n    -'
        for facts in self.consume(FirewalldFacts):
            for table in facts.ebtablesTablesInUse:
                if not private.isEbtablesTableSupported(table):
                    unsupported_tables.append(table)
            for ipset_type in facts.ipsetTypesInUse:
                if not private.isIpsetTypeSupportedByNftables(ipset_type):
                    unsupported_ipset_types.append(ipset_type)

        if unsupported_tables:
            format_tuple = (
                list_separator_fmt,
                list_separator_fmt.join(list(set(unsupported_tables))),)
            create_report([
                reporting.Title('Firewalld is using an unsupported ebtables table.'),
                reporting.Summary('ebtables in RHEL-8 does not support these tables:{}{}'.format(*format_tuple)),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags([
                        reporting.Tags.FIREWALL,
                        reporting.Tags.SECURITY,
                        reporting.Tags.NETWORK
                ]),
                reporting.Flags([
                        reporting.Flags.INHIBITOR
                ]),
                reporting.Remediation(
                    hint='Remove firewalld direct rules that use these ebtables tables:{}{}'.format(*format_tuple)
                )
            ] + related)

        if unsupported_ipset_types:
            format_tuple = (
                list_separator_fmt,
                list_separator_fmt.join(list(set(unsupported_ipset_types))),)
            create_report([
                reporting.Title('Firewalld is using an unsupported ipset type.'),
                reporting.Summary(
                    'These ipset types are not supported by firewalld\'s nftables backend:{}{}'.format(*format_tuple)
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags([
                        reporting.Tags.FIREWALL,
                        reporting.Tags.SECURITY,
                        reporting.Tags.NETWORK
                ]),
                reporting.Flags([
                        reporting.Flags.INHIBITOR
                ]),
                reporting.Remediation(
                    hint='Remove ipsets of these types from firewalld:{}{}'.format(*format_tuple)
                )
            ] + related)


# repos/system_upgrade/el7toel8/actors/checkfstabxfsoptions/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkfstabxfsoptions
from leapp.models import StorageInfo
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckFstabXFSOptions(Actor):
    """
    Check the FSTAB file for the deprecated / removed XFS mount options.

    Some mount options for XFS have been deprecated on RHEL 7 and already
    removed on RHEL 8. If any such an option is present in the FSTAB,
    it's impossible to boot the RHEL 8 system without the manual update of the
    file.

    Check whether any of these options are present in the FSTAB file
    and inhibit the upgrade in such a case.
    """

    name = 'checkfstabxfsoptions'
    consumes = (StorageInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checkfstabxfsoptions.process()


# repos/system_upgrade/el7toel8/actors/checkgrep/actor.py

from leapp.actors import Actor
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckGrep(Actor):
    """
    Check if Grep is installed. If yes, write information about non-compatible changes.
    """

    name = 'checkgrep'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'grep'):
            create_report([
                reporting.Title('Grep has incompatible changes in the next major version'),
                reporting.Summary(
                    'If a file contains data improperly encoded for the current locale, and this is '
                    'discovered before any of the file\'s contents are output, grep now treats the file '
                    'as binary.\n'
                    'The \'grep -P\' no longer reports an error and exits when given invalid UTF-8 data. '
                    'Instead, it considers the data to be non-matching.\n'
                    'In locales with multibyte character encodings other than UTF-8, grep -P now reports '
                    'an error and exits instead of misbehaving.\n'
                    'When searching binary data, grep now may treat non-text bytes as line terminators. '
                    'This can boost performance significantly.\n'
                    'The \'grep -z\' no longer automatically treats the byte \'\\200\' as binary data.\n'
                    'Context no longer excludes selected lines omitted because of -m. For example, '
                    '\'grep "^" -m1 -A1\' now outputs the first two input lines, not just the first '
                    'line.\n'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags([reporting.Tags.TOOLS]),
                reporting.Remediation(hint='Please update your scripts to be compatible with the changes.'),
                reporting.RelatedResource('package', 'grep')
            ])


# repos/system_upgrade/el7toel8/actors/checkhacluster/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkhacluster import check_ha_cluster
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class Checkhacluster(Actor):
    """
    Check if HA Cluster is in use. If yes, inhibit the upgrade process.

    The system is considered as part of cluster if a corosync.conf file
    (/etc/corosync/corosync.conf) can be found there.
    Also the system can be a part of a cluster as a remote node. In such case
    a cib file (/var/lib/pacemaker/cib/cib.xml) can be found there.
    """

    name = "check_ha_cluster"
    consumes = ()
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        check_ha_cluster()


# repos/system_upgrade/el7toel8/actors/checkirssi/actor.py

from leapp.actors import Actor
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp.reporting import Report, create_report
from leapp import reporting


class CheckIrssi(Actor):
    """
    Check if irssi is installed. If yes, write information about non-compatible changes.
    """

    name = 'checkirssi'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'irssi'):
            create_report([
                reporting.Title('Irssi incompatible changes in the next major version'),
                reporting.Summary(
                    'Disabled support for the insecure SSLv2 protocol.\n'
                    'Disabled SSLv3 due to the POODLE vulnerability.\n'
                    'Removing networks will now remove all attached servers and channels.\n'
                    'Removed --disable-ipv6 option.\n'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags([
                        reporting.Tags.COMMUNICATION,
                        reporting.Tags.TOOLS
                ]),
                reporting.Remediation(hint='Please update your scripts to be compatible with the changes.'),
                reporting.RelatedResource('package', 'irssi')
            ])


# repos/system_upgrade/el7toel8/actors/checkkdeapps/actor.py

from leapp.actors import Actor
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
from leapp.models import InstalledKdeAppsFacts, InstalledRPM
from leapp.libraries.actor.checkkdeapps import get_kde_apps_info


class CheckKdeApps(Actor):
    """
    Actor checks which KDE apps are installed.
    """

    name = 'check_kde_apps'
    consumes = (InstalledRPM,)
    produces = (InstalledKdeAppsFacts,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        app_facts = get_kde_apps_info()
        self.produce(InstalledKdeAppsFacts(
            installed_apps=app_facts))


# repos/system_upgrade/el7toel8/actors/checkkdegnome/actor.py

"""
Actor to check if KDE and/or GNOME are installed
Author: Jan Beran
Email: jaberan@redhat.com
"""

from leapp.actors import Actor
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag, ChecksPhaseTag
from leapp.libraries.actor.checkkdegnome import check_kde_gnome
from leapp.models import InstalledDesktopsFacts, InstalledKdeAppsFacts


class CheckKdeGnome(Actor):
    """
    Checks whether KDE is installed

    Actor will check whether KDE is installed together with GNOME desktop to inform whether we can
    inhibit the upgrade process. When both are installed, we need to inform the user that KDE will
    be removed and GNOME will be used instead. If only KDE is installed, we want to inhibit
    the upgrade process otherwise the user will end up without a desktop.
    Note: The Package Evolution Service data makes sure the KDE-related packages are removed in the
    dnf upgrade transaction.
    """
    name = 'check_kde_gnome'
    consumes = (InstalledDesktopsFacts, InstalledKdeAppsFacts)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        check_kde_gnome()


# repos/system_upgrade/el7toel8/actors/checkleftoverpackages/actor.py

from leapp.actors import Actor
from leapp.libraries.common.rpms import get_installed_rpms
from leapp.models import LeftoverPackages, TransactionCompleted, InstalledUnsignedRPM, RPM
from leapp.tags import RPMUpgradePhaseTag, IPUWorkflowTag


class CheckLeftoverPackages(Actor):
    """
    Check if there are any RHEL 7 packages present after upgrade.

    Actor produces message containing these packages. Message is empty if there are no el7 package left.
    """

    name = 'check_leftover_packages'
    consumes = (TransactionCompleted, InstalledUnsignedRPM)
    produces = (LeftoverPackages,)
    tags = (RPMUpgradePhaseTag, IPUWorkflowTag)

    def process(self):
        LEAPP_PACKAGES = ['leapp', 'leapp-repository', 'snactor', 'leapp-repository-deps-el8', 'leapp-deps-el8',
                          'python2-leapp']
        installed_rpms = get_installed_rpms()
        if not installed_rpms:
            return

        to_remove = LeftoverPackages()
        unsigned = [pkg.name for pkg in next(self.consume(InstalledUnsignedRPM), InstalledUnsignedRPM()).items]

        for rpm in installed_rpms:
            rpm = rpm.strip()
            if not rpm:
                continue
            name, version, release, epoch, packager, arch, pgpsig = rpm.split('|')

            if 'el7' in release and name not in set(unsigned + LEAPP_PACKAGES):
                to_remove.items.append(RPM(
                    name=name,
                    version=version,
                    epoch=epoch,
                    packager=packager,
                    arch=arch,
                    release=release,
                    pgpsig=pgpsig
                ))

        self.produce(to_remove)


# repos/system_upgrade/el7toel8/actors/checkmemcached/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkmemcached import check_memcached
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckMemcached(Actor):
    """
    Check for incompatible changes in memcached configuration.

    Warn that memcached in RHEL8 no longer listens on the UDP port by default
    and the default service configuration binds memcached to the loopback
    interface.
    """

    name = 'check_memcached'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        check_memcached(has_package(InstalledRedHatSignedRPM, 'memcached'))


# repos/system_upgrade/el7toel8/actors/checkmemory/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkmemory
from leapp.models import MemoryInfo, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckMemory(Actor):
    """
    The actor check the size of RAM against RHEL8 minimal hardware requirements

    Using the following resource: https://access.redhat.com/articles/rhel-limits
    """

    name = 'checkmemory'
    consumes = (MemoryInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checkmemory.process()


# repos/system_upgrade/el7toel8/actors/checkmultiplepackageversions/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkmultiplepackageversions import check
from leapp.models import InstalledRPM
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag, ChecksPhaseTag


class CheckMultiplePackageVersions(Actor):
    """
    Check for problematic 32bit packages installed together with 64bit ones.

    If a known problematic 32bit package is found, the upgrade will be inhibited with the detailed
    report how to solve the problem if such a remedy exists.
    """

    name = 'multiple_package_versions'
    consumes = (InstalledRPM,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        check()


# repos/system_upgrade/el7toel8/actors/checkntp/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkntp import check_ntp
from leapp.models import Report, InstalledRedHatSignedRPM, NtpMigrationDecision
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckNtp(Actor):
    """
    Check if ntp and/or ntpdate configuration needs to be migrated.
    """

    name = 'check_ntp'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report, NtpMigrationDecision)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        installed_packages = set()

        signed_rpms = self.consume(InstalledRedHatSignedRPM)
        for rpm_pkgs in signed_rpms:
            for pkg in rpm_pkgs.items:
                installed_packages.add(pkg.name)

        self.produce(check_ntp(installed_packages))


# repos/system_upgrade/el7toel8/actors/checkpcidrivers/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkpcidrivers import checkpcidrivers_main
from leapp.models import PCIDevices, RestrictedPCIDevices
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckPCIDrivers(Actor):
    """
    Check if host PCI devices drivers has restrictions on target system.

    If driver is restricted this means it is either unsupported or unavailable
    in some set of newer RHEL versions.

    If the driver is unavailable on a target system - then the upgrade will
        be inhibited
    If the driver is available, but not supported - then the upgrade will
        continue, but the relevant report will be generated
    If the driver is available and supported - then the upgrade will continue
        normally
    """

    name = "check_pci_drivers"
    consumes = (
        PCIDevices,
        RestrictedPCIDevices,
    )
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checkpcidrivers_main()


# repos/system_upgrade/el7toel8/actors/checkpostfix/actor.py

from leapp.actors import Actor
from leapp.models import InstalledRedHatSignedRPM
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckPostfix(Actor):
    """
    Check if postfix is installed, check whether configuration update is needed.
    """

    name = 'check_postfix'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        for fact in self.consume(InstalledRedHatSignedRPM):
            for rpm in fact.items:
                if rpm.name == 'postfix':
                    create_report([
                        reporting.Title('Postfix has incompatible changes in the next major version'),
                        reporting.Summary(
                            'Postfix 3.x has so called "compatibility safety net" that runs Postfix programs '
                            'with backwards-compatible default settings. It will log a warning whenever '
                            'backwards-compatible default setting may be required for continuity of service. '
                            'Based on this logging the system administrator can decide if any '
                            'backwards-compatible settings need to be made permanent in main.cf or master.cf, '
                            'before turning off the backwards-compatibility safety net.\n'
                            'The backward compatibility safety net is by default turned off in Red Hat '
                            'Enterprise Linux 8.\n'
                            'It can be turned on by running:  "postconf -e compatibility_level=0\n'
                            'It can be turned off by running: "postconf -e compatibility_level=2\n\n'
                            'In the Postfix MySQL database client, the default "option_group" value has changed '
                            'to "client", i.e. it now reads options from the [client] group from the MySQL '
                            'configuration file. To disable it, set "option_group" to the empty string.\n\n'
                            'The postqueue command no longer forces all message arrival times to be reported '
                            'in UTC. To get the old behavior, set TZ=UTC in main.cf:import_environment.\n\n'
                            'Postfix 3.2 enables elliptic curve negotiation. This changes the default '
                            'smtpd_tls_eecdh_grade setting to "auto", and introduces a new parameter '
                            '"tls_eecdh_auto_curves" with the names of curves that may be negotiated.\n\n'
                            'The "master.cf" chroot default value has changed from "y" (yes) to "n" (no). '
                            'This applies to master.cf services where chroot field is not explicitly '
                            'specified.\n\n'
                            'The "append_dot_mydomain" default value has changed from "yes" to "no". You may '
                            'need changing it to "yes" if senders cannot use complete domain names in e-mail '
                            'addresses.\n\n'
                            'The "relay_domains" default value has changed from "$mydestination" to the empty '
                            'value. This could result in unexpected "Relay access denied" errors or ETRN errors, '
                            'because now will postfix by default relay only for the localhost.\n\n'
                            'The "mynetworks_style" default value has changed from "subnet" to "host". '
                            'This parameter is used to implement the "permit_mynetworks" feature. The change '
                            'could result in unexpected "access denied" errors, because postfix will now by '
                            'default trust only the local machine, not the remote SMTP clients on the '
                            'same IP subnetwork.\n\n'
                            'Postfix now supports dynamically loaded database plugins. Plugins are shipped '
                            'in individual RPM sub-packages. Correct database plugins have to be installed, '
                            'otherwise the specific database client will not work. For example for PostgreSQL '
                            'map to work, the postfix-pgsql RPM package has to be installed.\n',
                        ),
                        reporting.Severity(reporting.Severity.LOW),
                        reporting.Tags([reporting.Tags.SERVICES, reporting.Tags.EMAIL]),
                        reporting.RelatedResource('package', 'postfix')
                    ])
                    return


# repos/system_upgrade/el7toel8/actors/checkremovedenvvars/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkremovedenvvars
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckRemovedEnvVars(Actor):
    """
    Check for usage of removed environment variables and inhibit the upgrade
    if they are used.
    """

    name = 'check_removed_envvars'
    consumes = ()
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checkremovedenvvars.process()


# repos/system_upgrade/el7toel8/actors/checkremovedpammodules/actor.py

from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.models import Report, PamConfiguration
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp.libraries.stdlib import api
from leapp.reporting import create_report
from leapp import reporting


class CheckRemovedPamModules(Actor):
    """
    Check for modules that are not available in RHEL 8 anymore

    At this moment, we check only for pam_tally2. Few more modules
    are alredy covered in RemoveOldPAMModulesApply actor
    """

    name = 'removed_pam_modules'
    consumes = (PamConfiguration, )
    produces = (Report, )
    tags = (ChecksPhaseTag, IPUWorkflowTag, )

    def process(self):
        messages = self.consume(PamConfiguration)
        config = next(messages, None)
        if list(messages):
            api.current_logger().warning('Unexpectedly received more than one PamConfiguration message.')
        if not config:
            raise StopActorExecutionError(
                'Could not check pam configuration', details={'details': 'No PamConfiguration facts found.'}
            )

        # This list contain tupples of removed modules and their recommended replacements
        removed_modules = [
            ('pam_tally2', 'pam_faillock'),
            ]
        found_services = set()
        found_modules = set()
        replacements = set()
        for service in config.services:
            for module in removed_modules:
                removed = module[0]
                replacement = module[1]
                if removed in service.modules:
                    found_services.add(service.service)
                    found_modules.add(removed)
                    replacements.add(replacement)

        if found_modules:
            create_report([
                reporting.Title('The {} pam module(s) no longer available'.format(', '.join(found_modules))),
                reporting.Summary('The services {} using PAM are configured to '
                                  'use {} module(s), which is no longer available '
                                  'in Red Hat Enterprise Linux 8.'.format(
                                      ', '.join(found_services), ', '.join(found_modules))),
                reporting.Remediation(
                    hint='If you depend on its functionality, it is '
                         'recommended to migrate to {}. Otherwise '
                         'please remove the pam module(s) from all the files '
                         'under /etc/pam.d/.'.format(', '.join(replacements))
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Flags([reporting.Flags.INHIBITOR]),
            ] + [reporting.RelatedResource('pam', r) for r in replacements | found_modules])


# repos/system_upgrade/el7toel8/actors/checksaphana/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checksaphana import perform_check
from leapp.models import SapHanaInfo
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckSapHana(Actor):
    """
    If SAP HANA has been detected, several checks are performed to ensure a successful upgrade.

    If the upgrade flavour is 'default' no checks are being executed.

    The following checks are executed:
    - If this system is _NOT_ running on x86_64, the upgrade is inhibited.
    - If SAP HANA 1 has been detected on the system the upgrade is inhibited since it is not supported on RHEL8.
    - If SAP HANA 2 has been detected, the upgrade will be inhibited if an unsupported version for the target release
      has been detected.
    - If SAP HANA is running the upgrade is inhibited.
    """

    name = 'check_sap_hana'
    consumes = (SapHanaInfo,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        perform_check()


# repos/system_upgrade/el7toel8/actors/checksendmail/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checksendmail
from leapp.libraries.common.rpms import has_package
from leapp.libraries.common.tcpwrappersutils import config_applies_to_daemon
from leapp.models import InstalledRedHatSignedRPM, SendmailMigrationDecision, TcpWrappersFacts
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag

COMMON_REPORT_TAGS = [reporting.Tags.SERVICES, reporting.Tags.EMAIL]

related = [
              reporting.RelatedResource('file', f) for f in checksendmail.get_conf_files()
          ] + [reporting.RelatedResource('package', 'sendmail')]


class CheckSendmail(Actor):
    """
    Check if sendmail is installed, check whether configuration update is needed, inhibit upgrade if TCP wrappers
    are used.
    """

    name = 'check_sendmail'
    consumes = (InstalledRedHatSignedRPM, TcpWrappersFacts,)
    produces = (Report, SendmailMigrationDecision,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if not has_package(InstalledRedHatSignedRPM, 'sendmail'):
            return

        if config_applies_to_daemon(next(self.consume(TcpWrappersFacts)), 'sendmail'):
            create_report([
                reporting.Title('TCP wrappers support removed in the next major version'),
                reporting.Summary(
                    'TCP wrappers are legacy host-based ACL (Access Control List) system '
                    'which has been removed in the next major version of RHEL.'
                ),
                reporting.Remediation(
                    hint='Please migrate from TCP wrappers to some other access control mechanism and delete '
                         'sendmail from the /etc/hosts.[allow|deny].'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags(COMMON_REPORT_TAGS + [reporting.Tags.NETWORK]),
                reporting.Flags([reporting.Flags.INHIBITOR])
            ] + related)

            return
        migrate_files = checksendmail.check_files_for_compressed_ipv6()
        if migrate_files:
            create_report([
                reporting.Title('sendmail configuration will be migrated'),
                reporting.Summary(
                    'IPv6 addresses will be uncompressed, check all IPv6 addresses in all sendmail '
                    'configuration files for correctness.'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags(COMMON_REPORT_TAGS)
            ] + related)

            self.produce(SendmailMigrationDecision(migrate_files=migrate_files))
        else:
            self.log.info('The sendmail configuration seems compatible - it won\'t be migrated.')


# repos/system_upgrade/el7toel8/actors/checkwireshark/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM, Report
from leapp.reporting import create_report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckWireshark(Actor):
    """
    Report a couple of changes in tshark usage
    """

    name = 'check_wireshark'
    consumes = (InstalledRedHatSignedRPM, )
    produces = (Report, )
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'wireshark'):
            create_report([
                reporting.Title('tshark: CLI options and output changes'),
                reporting.Summary(
                    'The -C suboption for -N option for asynchronous DNS name resolution '
                    'has been completely removed from tshark. The reason for this is that '
                    'the asynchronous DNS resolution is now the only resolution available '
                    'so there is no need for -C. If you are using -NC with tshark in any '
                    'of your scripts, please remove it.'
                    '\n\n'
                    'When using -H option with capinfos, the output no longer shows MD5 hashes. '
                    'Now it shows SHA256 instead. SHA1 might get removed very soon as well. '
                    'If you use these output values, please change your scripts.'),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags([reporting.Tags.MONITORING, reporting.Tags.SANITY, reporting.Tags.TOOLS]),
                reporting.RelatedResource('package', 'wireshark'),
            ])


# repos/system_upgrade/el7toel8/actors/cupscheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import cupscheck
from leapp.models import CupsChangedFeatures, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CupsCheck(Actor):
    """
    Reports changes in configuration between CUPS 1.6.3 and 2.2.6

    Reports if user configuration contains features (interface scripts),
    directives (Include, PrintcapFormat, PassEnv, SetEnv,
    ServerCertificate, ServerKey) or directive values (Digest,
    BasicDigest). Some of them were removed for security reasons
    (interface scripts and directive Include), moved
    to cups-files.conf for security reasons (PassEnv, SetEnv).
    Others were removed (ServerCertificate, ServerKey, Digest,
    BasicDigest) or moved (PrintcapFormat) due deprecation.
    """

    name = 'cups_check'
    consumes = (CupsChangedFeatures,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        cupscheck.make_reports()


# repos/system_upgrade/el7toel8/actors/cupsfiltersmigrate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import cupsfiltersmigrate
from leapp.models import InstalledRedHatSignedRPM
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class CupsfiltersMigrate(Actor):
    """
    Actor for migrating package cups-filters.

    Migrating cups-filters package means adding two directives into
    /etc/cups/cups-browsed.conf - LocalQueueNamingRemoteCUPS and
    CreateIPPPrinterQueues.

    LocalQueueNamingRemoteCUPS directive indicates what will be used as a name
    for local print queue creation - the default is DNS-SD ID of advertised
    print queue now, it was the name of remote print queue in the past.

    CreateIPPPrinterQueues directive serves for telling cups-browsed to create
    local print queues for all available IPP printers.
    """

    name = 'cupsfilters_migrate'
    consumes = (InstalledRedHatSignedRPM,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        cupsfiltersmigrate.update_cups_browsed()


# repos/system_upgrade/el7toel8/actors/cupsmigrate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import cupsmigrate
from leapp.models import CupsChangedFeatures
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class CupsMigrate(Actor):
    """
    cups_migrate actor

    Migrates configuration directives and writes into error log
    if any error was encountered.
    """

    name = 'cups_migrate'
    consumes = (CupsChangedFeatures,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        cupsmigrate.migrate_configuration()


# repos/system_upgrade/el7toel8/actors/cupsscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import cupsscanner
from leapp.models import CupsChangedFeatures, InstalledRedHatSignedRPM, Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class CupsScanner(Actor):
    """
    Gather facts about CUPS features which needs to be migrated

    Actor checks if cups package is installed and if one or more following
    situations appears in configuration files:
    - interface scripts
    - use of 'Digest' or 'BasicDigest' authentication
    - use of 'Include' directive
    - use of 'ServerCertificate' and 'ServerKey' directives
    - use of 'SetEnv' or 'PassEnv' directives
    - use of 'PrintcapFormat' directive

    The actor creates list from gathered data.
    """

    name = 'cups_scanner'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report, CupsChangedFeatures)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        cupsscanner.find_features()


# repos/system_upgrade/el7toel8/actors/detectgrubconfigerror/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.scanner import detect_config_error
from leapp.libraries.common.config import architecture
from leapp.models import GrubConfigError
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class DetectGrubConfigError(Actor):
    """
    Check grub configuration for syntax error in GRUB_CMDLINE_LINUX value.
    """

    name = 'detect_grub_config_error'
    consumes = ()
    produces = (Report, GrubConfigError)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if architecture.matches_architecture(architecture.ARCH_S390X):
            # For now, skip just s390x, that's only one that is failing now
            # because ZIPL is used there
            return
        config = '/etc/default/grub'
        error_detected = detect_config_error(config)
        if error_detected:
            create_report([
                reporting.Title('Syntax error detected in grub configuration'),
                reporting.Summary(
                    'Syntax error was detected in GRUB_CMDLINE_LINUX value of grub configuration. '
                    'This error is causing booting and other issues. '
                    'Error is automatically fixed by add_upgrade_boot_entry actor.'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags([reporting.Tags.BOOT]),
                reporting.RelatedResource('file', config)
            ])

        self.produce(GrubConfigError(error_detected=error_detected))


# repos/system_upgrade/el7toel8/actors/firewalldfactsactor/actor.py

import os
from xml.etree import ElementTree

from leapp.actors import Actor
from leapp.libraries.actor import firewalldfactsactor
from leapp.models import FirewalldFacts
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class FirewalldFactsActor(Actor):
    """
    Provide data about firewalld

    After collecting data, a message with relevant data will be produced.
    """

    name = 'firewalld_facts_actor'
    consumes = ()
    produces = (FirewalldFacts,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        facts = FirewalldFacts()

        try:
            tree = ElementTree.parse('/etc/firewalld/lockdown-whitelist.xml')
            root = tree.getroot()
            facts.firewall_config_command = firewalldfactsactor.getLockdownFirewallConfigCommand(root)
        except IOError:
            pass

        try:
            tree = ElementTree.parse('/etc/firewalld/direct.xml')
            root = tree.getroot()
            facts.ebtablesTablesInUse = firewalldfactsactor.getEbtablesTablesInUse(root)
        except IOError:
            pass

        ipsetTypesInUse = set()
        directory = '/etc/firewalld/ipsets'
        try:
            for filename in os.listdir(directory):
                if not filename.endswith('.xml'):
                    continue
                try:
                    tree = ElementTree.parse(os.path.join(directory, filename))
                    root = tree.getroot()
                    ipsetTypesInUse |= set(firewalldfactsactor.getIpsetTypesInUse(root))
                except IOError:
                    pass
            facts.ipsetTypesInUse = list(ipsetTypesInUse)
        except OSError:
            pass

        self.produce(facts)


# repos/system_upgrade/el7toel8/actors/firewalldupdatelockdownwhitelist/actor.py

import xml.etree.ElementTree as ElementTree

from leapp.actors import Actor
from leapp.libraries.actor import firewalldupdatelockdownwhitelist
from leapp.models import FirewalldFacts
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class FirewalldUpdateLockdownWhitelist(Actor):
    """
    Update the firewalld Lockdown Whitelist.

    RHEL-8 uses a platform specific python interpreter for packaged
    applications. For firewall-config, the interpreter path is part of the
    lockdown list. In RHEL-7 this was simply /usr/bin/python, but in RHEL-8
    it's /usr/libexec/platform-python. However, if the user made changes to the
    lockdown whitelist it won't be replaced by RPM/dnf. As such we must update
    the interpreter if the old value is there.
    """

    name = 'firewalld_update_lockdown_whitelist'
    consumes = (FirewalldFacts,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        for facts in self.consume(FirewalldFacts):
            if facts.firewall_config_command:
                tree = ElementTree.parse('/etc/firewalld/lockdown-whitelist.xml')
                root = tree.getroot()

                need_write = firewalldupdatelockdownwhitelist.updateFirewallConfigCommand(
                    root,
                    facts.firewall_config_command
                )

                if need_write:
                    tree.write('/etc/firewalld/lockdown-whitelist.xml')
                    self.log.info('Updated lockdown whitelist')


# repos/system_upgrade/el7toel8/actors/grubdevname/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.grubdevname import get_grub_device
from leapp.libraries.common.config import architecture
from leapp.models import GrubDevice
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
from leapp.utils.deprecation import suppress_deprecation


# TODO: remove this actor completely after the deprecation period expires
@suppress_deprecation(GrubDevice)
class Grubdevname(Actor):
    """
    Get name of block device where GRUB is located
    """

    name = 'grubdevname'
    consumes = ()
    produces = (GrubDevice,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        if architecture.matches_architecture(architecture.ARCH_S390X):
            return
        get_grub_device()


# repos/system_upgrade/el7toel8/actors/migratebrltty/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import migratebrltty
from leapp.models import BrlttyMigrationDecision
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class MigrateBrltty(Actor):
    """
    Migrate brltty configuration files.
    """

    name = 'migrate_brltty'
    consumes = (BrlttyMigrationDecision,)
    produces = (Report,)
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        for decision in self.consume(BrlttyMigrationDecision):
            report_summary = ''
            migratebrltty.migrate_file(decision.migrate_file, decision.migrate_bt, decision.migrate_espeak)
            if decision.migrate_bt:
                report_summary = 'Unsupported aliases for bluetooth devices (\'bth:\' and \'bluez:\') was '
                report_summary += 'renamed to \'bluetooth:\' in {}'
                report_summary = report_summary.format(', '.join(decision.migrate_file))
            if decision.migrate_espeak:
                if report_summary:
                    report_summary += '\n'
                report_summary += 'eSpeak speech driver was switched to eSpeak-NG in {}'
                report_summary = report_summary.format(', '.join(decision.migrate_file))
            if decision.migrate_bt or decision.migrate_espeak:
                create_report([
                    reporting.Title('brltty configuration files migrated'),
                    reporting.Summary(report_summary),
                    reporting.Severity(reporting.Severity.LOW),
                    reporting.Tags([reporting.Tags.TOOLS, reporting.Tags.ACCESSIBILITY]),
                    reporting.RelatedResource('package', 'brltty')
                ])


# repos/system_upgrade/el7toel8/actors/migratentp/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.migratentp import migrate_ntp
from leapp.models import Report, NtpMigrationDecision
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class MigrateNtp(Actor):
    """
    Migrate ntp and/or ntpdate configuration to chrony.
    """

    name = 'migrate_ntp'
    consumes = (NtpMigrationDecision,)
    produces = (Report,)
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        for decision in self.consume(NtpMigrationDecision):
            migrate_ntp(decision.migrate_services, decision.config_tgz64)


# repos/system_upgrade/el7toel8/actors/migratesendmail/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import migratesendmail
from leapp.models import SendmailMigrationDecision
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class MigrateSendmail(Actor):
    """
    Migrate sendmail configuration files.
    """

    name = 'migrate_sendmail'
    consumes = (SendmailMigrationDecision,)
    produces = (Report,)
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        decision = next(self.consume(SendmailMigrationDecision), None)
        if not decision or not decision.migrate_files:
            return

        for f in decision.migrate_files:
            migratesendmail.migrate_file(f)
        list_separator_fmt = '\n    - '
        create_report([
            reporting.Title('sendmail configuration files migrated'),
            reporting.Summary(
                'Uncompressed IPv6 addresses in {}'.format(list_separator_fmt.join(decision.migrate_files))
            ),
            reporting.Severity(reporting.Severity.LOW),
            reporting.Tags([reporting.Tags.SERVICES, reporting.Tags.EMAIL])
        ])


# repos/system_upgrade/el7toel8/actors/multipathconfcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import multipathconfcheck
from leapp.models import MultipathConfFacts
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class MultipathConfCheck(Actor):
    """
    Checks whether the multipath configuration can be updated to RHEL-8 and
    plan necessary tasks.

    Specifically, it checks if the path_checker/checker option is set to
    something other than tur in the defaults section. If so, non-trivial
    changes may be required in the multipath.conf file, and it is not
    possible to auto-update it - in such a case inhibit upgrade.

    In addition create a task to ensure that configuration files are copied
    into the target container (they are necessary for correct creation of the
    upgrade initramfs.
    """

    name = 'multipath_conf_check'
    consumes = (MultipathConfFacts,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        facts = next(self.consume(MultipathConfFacts), None)
        if facts is None:
            self.log.debug('Skipping execution. No MultipathConfFacts has '
                           'been produced')
            return
        multipathconfcheck.check_configs(facts)


# repos/system_upgrade/el7toel8/actors/multipathconfupdate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import multipathconfupdate
from leapp.models import MultipathConfFacts
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class MultipathConfUpdate(Actor):
    """
    Modifies multipath configuration files on the target RHEL-8 system so that
    they will run properly. This is done in three ways
    1. commenting out lines for options that no longer exist, or whose value
       is no longer current in RHEL-8
    2. Migrating any options in an devices section with all_devs to an
       overrides setions
    3. Rename options that have changed names
    """

    name = 'multipath_conf_update'
    consumes = (MultipathConfFacts,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        facts = next(self.consume(MultipathConfFacts), None)
        if facts is None:
            self.log.debug('Skipping execution. No MultipathConfFacts has '
                           'been produced')
            return
        multipathconfupdate.update_configs(facts)


# repos/system_upgrade/el7toel8/actors/networkmanagerreadconfig/actor.py

from leapp.libraries.actor.networkmanagerreadconfig import read_nm_config, parse_nm_config, check_nm_dhcp
from leapp.actors import Actor
from leapp.models import NetworkManagerConfig
from leapp.tags import IPUWorkflowTag, FactsPhaseTag


class NetworkManagerReadConfig(Actor):
    """
    Provides data about NetworkManager configuration.

    After collecting data from NetworkManager tool, a message with relevant data will be produced.
    """

    name = 'network_manager_read_config'
    consumes = ()
    produces = (NetworkManagerConfig,)
    tags = (IPUWorkflowTag, FactsPhaseTag,)

    def process(self):
        nm_config = NetworkManagerConfig()

        cfg = read_nm_config()
        parser = parse_nm_config(cfg)

        if parser:
            check_nm_dhcp(nm_cfg=nm_config, parser=parser)

            self.produce(nm_config)


# repos/system_upgrade/el7toel8/actors/networkmanagerupdateconfig/actor.py

from leapp.actors import Actor
from leapp.models import NetworkManagerConfig
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


snippet_path = '/usr/lib/NetworkManager/conf.d/10-dhcp-dhclient.conf'
snippet_data = ("# Generated by leapp when upgrading from RHEL7 to RHEL8\n"
                "[main]\n"
                "dhcp=dhclient\n")


class NetworkManagerUpdateConfig(Actor):
    """
    Updates NetworkManager configuration for Red Hat Enterprise Linux 8.

    On Red Hat Enterprise Linux 7 NetworkManager uses the "dhclient" DHCP backend by default, while
    the default is "internal" on Red Hat Enterprise Linux 8. We want to keep "dhclient" enabled on
    upgrade, unless the user explicitly chose another backend in the configuration. To do so, we
    drop a configuration snippet in /usr/lib.
    """

    name = 'network_manager_update_config'
    consumes = (NetworkManagerConfig,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        for nm_config in self.consume(NetworkManagerConfig):
            self.log.info('Consuming dhcp={}'.format(nm_config.dhcp))
            if nm_config.dhcp == '':
                try:
                    with open(snippet_path, 'w') as f:
                        f.write(snippet_data)
                        self.log.info('Written the following to {}:\n{}\n'.format(snippet_path, snippet_data))
                except IOError as e:
                    self.log.warning('Write error: {}'.format(e))
            break


# repos/system_upgrade/el7toel8/actors/networkmanagerupdateconnections/actor.py

from leapp.actors import Actor
from leapp.libraries.stdlib import CalledProcessError, run
from leapp.models import NetworkManagerConfig
from leapp.tags import FirstBootPhaseTag, IPUWorkflowTag


class NetworkManagerUpdateConnections(Actor):
    """
    Update NetworkManager connections.

    When using dhcp=dhclient on Red Hat Enterprise Linux 7, a non-hexadecimal client-id (a string)
    is sent on the wire as is (i.e. the first character is the 'type' as per RFC 2132 section
    9.14). On Red Hat Enterprise Linux 8, a zero byte is prepended to string-only client-ids. To
    preserve behavior on upgrade, we convert client-ids to the hexadecimal form.
    """

    name = 'network_manager_update_connections'
    consumes = (NetworkManagerConfig,)
    produces = ()
    tags = (FirstBootPhaseTag, IPUWorkflowTag)

    def process(self):
        for nm_config in self.consume(NetworkManagerConfig):
            if nm_config.dhcp not in ('', 'dhclient'):
                self.log.info('DHCP client is "{}", nothing to do'.format(nm_config.dhcp))
                return

            try:
                r = run(['/usr/bin/python3', 'tools/nm-update-client-ids.py'])['stdout']
                self.log.info('Updated client-ids: {}'.format(r))
            except (OSError, CalledProcessError) as e:
                self.log.warning('Error calling nm-update-client-ids script: {}'.format(e))

            break


# repos/system_upgrade/el7toel8/actors/networkmanagerupdateservice/actor.py

from leapp.actors import Actor
from leapp.libraries.stdlib import CalledProcessError, run
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class NetworkManagerUpdateService(Actor):
    """
    Updates NetworkManager services status.

    On Red Hat Enterprise Linux 7 if the NetworkManager service was disabled and
    NetworkManager-wait-online enabled, the former would not be started. This changed on Red Hat
    Enterprise Linux 8, where NM-w-o 'Requires' NM and so NM can be started even if disabled. Upon
    upgrade, to keep the previous behavior we must disable NM-w-o when NM is disabled.

    See also:
      https://bugzilla.redhat.com/show_bug.cgi?id=1520865
    """

    name = 'network_manager_update_service'
    consumes = ()
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        nm_enabled = self.unit_enabled('NetworkManager.service')
        nmwo_enabled = self.unit_enabled('NetworkManager-wait-online.service')
        self.log_services_state('initial', nm_enabled, nmwo_enabled)

        if not nm_enabled and nmwo_enabled:
            self.log.info('Disabling NetworkManager-wait-online.service')

            try:
                run(['systemctl', 'disable', 'NetworkManager-wait-online.service'])
            except (OSError, CalledProcessError) as e:
                self.log.warning('Error disabling NetworkManager-wait-online.service: {}'.format(e))
                return

            nm_enabled = self.unit_enabled('NetworkManager.service')
            nmwo_enabled = self.unit_enabled('NetworkManager-wait-online.service')
            self.log_services_state('after upgrade', nm_enabled, nmwo_enabled)

    def log_services_state(self, detail, nm, nmwo):
        self.log.info('Services state ({}):'.format(detail))
        self.log.info(' - NetworkManager            : {}'.format('enabled' if nm else 'disabled'))
        self.log.info(' - NetworkManager-wait-online: {}'.format('enabled' if nmwo else 'disabled'))

    def unit_enabled(self, name):
        try:
            ret = run(['systemctl', 'is-enabled', name], split=True)['stdout']
            if ret:
                enabled = ret[0] == 'enabled'
            else:
                enabled = False
        except (OSError, CalledProcessError):
            enabled = False
        return enabled


# repos/system_upgrade/el7toel8/actors/opensshalgorithmscheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import opensshalgorithmscheck
from leapp.models import Report, OpenSshConfig
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class OpenSshAlgorithmsCheck(Actor):
    """
    OpenSSH configuration does not contain any unsupported cryptographic algorithms.

    Check the values of Ciphers and MACs in OpenSSH server config file and warn
    about removed algorithms which might cause the server to fail to start.
    """
    name = 'open_ssh_algorithms'
    consumes = (OpenSshConfig,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        opensshalgorithmscheck.process(self.consume(OpenSshConfig))


# repos/system_upgrade/el7toel8/actors/opensshconfigscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import readopensshconfig
from leapp.models import OpenSshConfig
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class OpenSshConfigScanner(Actor):
    """
    Collect information about the OpenSSH configuration.

    Currently supporting the following options:

     * PermitRootLogin
     * UsePrivilegeSeparation
     * Protocol
     * Ciphers
     * MACs

    """

    name = 'read_openssh_config'
    consumes = ()
    produces = (OpenSshConfig, )
    tags = (FactsPhaseTag, IPUWorkflowTag, )

    def process(self):
        readopensshconfig.scan_sshd(self.produce)


# repos/system_upgrade/el7toel8/actors/opensshdeprecateddirectivescheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.opensshdeprecateddirectivescheck import inhibit_if_deprecated_directives_used
from leapp.models import Report, OpenSshConfig
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class OpenSshDeprecatedDirectivesCheck(Actor):
    """
    Check for any deprecated directives in the OpenSSH configuration.

    Checks the directives used in the OpenSSH configuration for ones that have
    been deprecated and their usage in newer versions would result in the sshd
    service failing to start after the upgrade.
    """

    name = 'open_ssh_deprecated_directives_check'
    consumes = (OpenSshConfig,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        ssh_config = next(self.consume(OpenSshConfig))
        inhibit_if_deprecated_directives_used(ssh_config)


# repos/system_upgrade/el7toel8/actors/opensshpermitrootlogincheck/actor.py

from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor.opensshpermitrootlogincheck import semantics_changes
from leapp.models import Report, OpenSshConfig
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp.libraries.stdlib import api
from leapp.reporting import create_report
from leapp import reporting


COMMON_REPORT_TAGS = [
    reporting.Tags.AUTHENTICATION,
    reporting.Tags.SECURITY,
    reporting.Tags.NETWORK,
    reporting.Tags.SERVICES
]


class OpenSshPermitRootLoginCheck(Actor):
    """
    OpenSSH no longer allows root logins with password.

    Check the values of PermitRootLogin in OpenSSH server configuration file
    and warn about potential issues after update.
    """
    name = 'openssh_permit_root_login'
    consumes = (OpenSshConfig, )
    produces = (Report, )
    tags = (ChecksPhaseTag, IPUWorkflowTag, )

    def process(self):
        openssh_messages = self.consume(OpenSshConfig)
        config = next(openssh_messages, None)
        if list(openssh_messages):
            api.current_logger().warning('Unexpectedly received more than one OpenSshConfig message.')
        if not config:
            raise StopActorExecutionError(
                'Could not check openssh configuration', details={'details': 'No OpenSshConfig facts found.'}
            )

        resources = [
            reporting.RelatedResource('package', 'openssh-server'),
            reporting.RelatedResource('file', '/etc/ssh/sshd_config')
        ]
        if not config.permit_root_login:
            # TODO find out whether the file was modified and will be
            # replaced by the update. If so, this message is bogus
            create_report([
                reporting.Title('Possible problems with remote login using root account'),
                reporting.Summary(
                    'OpenSSH configuration file does not explicitly state '
                    'the option PermitRootLogin in sshd_config file, '
                    'which will default in RHEL8 to "prohibit-password".'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags(COMMON_REPORT_TAGS),
                reporting.Remediation(
                    hint='If you depend on remote root logins using '
                         'passwords, consider setting up a different '
                         'user for remote administration or adding '
                         '"PermitRootLogin yes" to sshd_config.'
                ),
                reporting.Flags([reporting.Flags.INHIBITOR])
            ] + resources)

        # Check if there is at least one PermitRootLogin other than "no"
        # in match blocks (other than Match All).
        # This usually means some more complicated setup depending on the
        # default value being globally "yes" and being overwritten by this
        # match block
        if semantics_changes(config):
            create_report([
                reporting.Title('OpenSSH configured to allow root login'),
                reporting.Summary(
                    'OpenSSH is configured to deny root logins in match '
                    'blocks, but not explicitly enabled in global or '
                    '"Match all" context. This update changes the '
                    'default to disable root logins using paswords '
                    'so your server migth get inaccessible.'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags(COMMON_REPORT_TAGS),
                reporting.Remediation(
                    hint='Consider using different user for administrative '
                         'logins or make sure your configration file '
                         'contains the line "PermitRootLogin yes" '
                         'in global context if desired.'
                ),
                reporting.Flags([reporting.Flags.INHIBITOR])
            ] + resources)


# repos/system_upgrade/el7toel8/actors/opensshprotocolcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import opensshprotocolcheck
from leapp.models import Report, OpenSshConfig
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class OpenSshProtocolCheck(Actor):
    """
    Protocol configuration option was removed.

    Check the value of Protocol in OpenSSH server config file
    and warn about its deprecation if it is set. This option was removed
    in RHEL 7.4, but it might still be hanging around.
    """

    name = 'open_ssh_protocol'
    consumes = (OpenSshConfig, )
    produces = (Report, )
    tags = (ChecksPhaseTag, IPUWorkflowTag, )

    def process(self):
        opensshprotocolcheck.process(self.consume(OpenSshConfig))


# repos/system_upgrade/el7toel8/actors/opensshuseprivilegeseparationcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import opensshuseprivilegeseparationcheck
from leapp.models import Report, OpenSshConfig
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class OpenSshUsePrivilegeSeparationCheck(Actor):
    """
    UsePrivilegeSeparation configuration option was removed.

    Check the value of UsePrivilegeSeparation in OpenSSH server config file
    and warn about its deprecation if it is set to non-default value.
    """
    name = 'open_ssh_use_privilege_separation'
    consumes = (OpenSshConfig, )
    produces = (Report, )
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        opensshuseprivilegeseparationcheck.process(self.consume(OpenSshConfig))


# repos/system_upgrade/el7toel8/actors/pammodulesscanner/actor.py

import os

from leapp.actors import Actor
from leapp.models import PamConfiguration, PamService
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
from leapp.libraries.stdlib import api
from leapp.libraries.common.pam import PAM


class PamModulesScanner(Actor):
    """
    Scan the pam directory for services and modules used in them

    This produces a PAMConfiguration message containing the whole
    list of configured PAM services and what modules they contain.
    """

    name = 'pam_modules_scanner'
    consumes = ()
    produces = (PamConfiguration, )
    tags = (FactsPhaseTag, IPUWorkflowTag, )

    def process(self):
        conf = []
        path = "/etc/pam.d/"
        for f in os.listdir(path):
            pam_file = os.path.join(path, f)
            # Ignore symlinks (usually handled by authconfig)
            if not os.path.isfile(pam_file) or os.path.islink(pam_file):
                continue

            # Use the existing PAM library to parse the files, but unpack it to our model
            try:
                content = PAM.read_file(pam_file)
                modules = PAM(content)
                service = PamService(service=f, modules=modules.modules)
                conf.append(service)
            except OSError as err:
                # if leapp can not read that file it will probably not be important
                api.current_logger().warning('Failed to read file {}: {}'.format(pam_file, err.strerror))

        pam = PamConfiguration(services=conf)
        self.produce(pam)


# repos/system_upgrade/el7toel8/actors/postgresqlcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.postgresqlcheck import report_installed_packages
from leapp.models import Report, InstalledRedHatSignedRPM
from leapp.tags import IPUWorkflowTag, ChecksPhaseTag


class PostgresqlCheck(Actor):
    """
    Actor checking for presence of PostgreSQL installation.

    Provides user with information related to upgrading systems
    with PostgreSQL installed.
    """
    name = 'postgresql_check'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        report_installed_packages()


# repos/system_upgrade/el7toel8/actors/powertop/actor.py

from leapp.actors import Actor
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class PowerTop(Actor):
    """
    Check if PowerTOP is installed. If yes, write information about non-compatible changes.
    """

    name = 'powertop'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'powertop'):
            create_report([
                reporting.Title('PowerTOP compatibility options removed in the next major version'),
                reporting.Summary(
                    'The -d (dump) option which has been kept for RHEL backward compatibility has been '
                    'dropped.\n'
                    'The -h option which has been used for RHEL backward compatibility is no longer '
                    'alias for --html, but it\'s now an alias for --help to follow the upstream.\n'
                    'The -u option which has been used for RHEL backward compatibility as an alias for '
                    '--help has been dropped.\n'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Tags([reporting.Tags.TOOLS, reporting.Tags.MONITORING]),
                reporting.Remediation(hint='Please remove the dropped options from your scripts.'),
                reporting.RelatedResource('package', 'powertop')
            ])


# repos/system_upgrade/el7toel8/actors/prepareyumconfig/actor.py

from leapp.actors import Actor
from leapp.libraries.common import utils
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class Prepareyumconfig(Actor):
    """
    Handle migration of the yum configuration files.

    RPM cannot handle replacement of directories by symlinks by default
    without the %pretrans scriptlet. As yum package is packaged wrong,
    we have to workround that by migration of the yum configuration files
    before the rpm transaction is processed.
    """

    name = 'prepareyumconfig'
    consumes = ()
    produces = ()
    tags = (IPUWorkflowTag, PreparationPhaseTag)

    def process(self):
        utils.apply_yum_workaround()


# repos/system_upgrade/el7toel8/actors/pythoninformuser/actor.py

from leapp.actors import Actor
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class PythonInformUser(Actor):
    name = "python_inform_user"
    description = "This actor informs the user of differences in Python version and support in RHEL 8."
    consumes = ()
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        url = "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/configuring_basic_system_settings/#using-python3"  # noqa: E501; pylint: disable=line-too-long
        title = "Difference in Python versions and support in RHEL 8"
        summary = ("In RHEL 8, there is no 'python' command."
                   " Python 3 (backward incompatible) is the primary Python version"
                   " and Python 2 is available with limited support and limited set of packages."
                   " Read more here: {}".format(url))
        create_report([
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([reporting.Tags.PYTHON]),
            reporting.Audience('developer'),
            reporting.ExternalLink(url, title),
            reporting.Remediation(hint='Please run "alternatives --set python /usr/bin/python3" after upgrade'),
            reporting.RelatedResource('package', 'python'),
            reporting.RelatedResource('package', 'python2'),
            reporting.RelatedResource('package', 'python3')
        ])


# repos/system_upgrade/el7toel8/actors/quaggadaemons/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.quaggadaemons import process_daemons
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM, QuaggaToFrrFacts
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class QuaggaDaemons(Actor):
    """
    Active quagga daemons check.

    Checking for daemons that are currently running in the system.
    These should be enabled in /etc/frr/daemons later in the process.
    The tools will check for config files later on since these should stay in the system.
    """

    name = 'quagga_daemons'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (QuaggaToFrrFacts,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        if has_package(InstalledRedHatSignedRPM, 'quagga'):
            self.produce(process_daemons())


# repos/system_upgrade/el7toel8/actors/quaggareport/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.models import QuaggaToFrrFacts, Report
from leapp.reporting import create_report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag

COMMON_REPORT_TAGS = [
    reporting.Tags.NETWORK,
    reporting.Tags.SERVICES
]


class QuaggaReport(Actor):
    """
    Checking for babeld on RHEL-7.

    This actor is supposed to report that babeld was used on RHEL-7
    and it is no longer available in RHEL-8.
    """

    name = 'quagga_report'
    consumes = (QuaggaToFrrFacts, )
    produces = (Report, )
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        try:
            quagga_facts = next(self.consume(QuaggaToFrrFacts))
        except StopIteration:
            return
        if 'babeld' in quagga_facts.active_daemons or 'babeld' in quagga_facts.enabled_daemons:
            create_report([
                reporting.Title('Babeld is not available in FRR'),
                reporting.ExternalLink(
                    url='https://access.redhat.com/'
                        'documentation/en-us/red_hat_enterprise_linux/8/html/'
                        'configuring_and_managing_networking/setting-your-rou'
                        'ting-protocols_configuring-and-managing-networking',
                    title='Setting routing protocols in RHEL8'),
                reporting.Summary(
                    'babeld daemon which was a part of quagga implementation in RHEL7 '
                    'is not available in RHEL8 in FRR due to licensing issues.'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Tags(COMMON_REPORT_TAGS),
                reporting.Flags([reporting.Flags.INHIBITOR]),
                reporting.Remediation(hint='Please use RIP, OSPF or EIGRP instead of Babel')
            ])
        else:
            self.log.debug('babeld not used, moving on.')


# repos/system_upgrade/el7toel8/actors/quaggatofrr/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.quaggatofrr import process_facts
from leapp.models import QuaggaToFrrFacts
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class QuaggaToFrr(Actor):
    """
    Edit frr configuration on the new system.

    Take gathered info about quagga from RHEL 7 and apply these to frr in RHEL 8.
    """

    name = 'quagga_to_frr'
    consumes = (QuaggaToFrrFacts, )
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        quagga_facts = next(self.consume(QuaggaToFrrFacts), None)

        if quagga_facts:
            process_facts(quagga_facts)


# repos/system_upgrade/el7toel8/actors/removeleftoverpackages/actor.py

from leapp.actors import Actor
from leapp.libraries import stdlib
from leapp.libraries.common import rhsm
from leapp.libraries.common.rpms import get_installed_rpms
from leapp.models import LeftoverPackages, RemovedPackages, RPM
from leapp.reporting import Report
from leapp.tags import RPMUpgradePhaseTag, IPUWorkflowTag, ExperimentalTag


class RemoveLeftoverPackages(Actor):
    """
    Remove el7 packages left on the system after the upgrade to RHEL 8.

    Removal of el7 packages is necessary in order to keep the machine in supported state.
    Actor generates report telling users what packages have been removed.
    """

    name = 'remove_leftover_packages'
    consumes = (LeftoverPackages, )
    produces = (Report, RemovedPackages)
    tags = (RPMUpgradePhaseTag, IPUWorkflowTag, ExperimentalTag)

    def process(self):
        leftover_packages = next(self.consume(LeftoverPackages), LeftoverPackages())
        if not leftover_packages.items:
            self.log.info('No leftover packages, skipping...')
            return

        installed_rpms = get_installed_rpms()

        to_remove = ['-'.join([pkg.name, pkg.version, pkg.release]) for pkg in leftover_packages.items]
        cmd = ['dnf', 'remove', '-y', '--noautoremove'] + to_remove
        if rhsm.skip_rhsm():
            # ensure we don't use suscription-manager when it should be skipped
            cmd += ['--disableplugin', 'subscription-manager']
        try:
            stdlib.run(cmd)
        except stdlib.CalledProcessError:
            error = 'Failed to remove packages: {}'.format(', '.join(to_remove))
            self.log.error(error)
            return

        removed_packages = RemovedPackages()
        removed = list(set(installed_rpms) - set(get_installed_rpms()))
        for pkg in removed:
            name, version, release, epoch, packager, arch, pgpsig = pkg.split('|')
            removed_packages.items.append(RPM(
                name=name,
                version=version,
                epoch=epoch,
                packager=packager,
                arch=arch,
                release=release,
                pgpsig=pgpsig
            ))
        self.produce(removed_packages)


# repos/system_upgrade/el7toel8/actors/removeoldpammodulesapply/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.removeoldpammodulesapply import comment_modules, read_file
from leapp.libraries.common.pam import PAM
from leapp.models import RemovedPAMModules
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class RemoveOldPAMModulesApply(Actor):
    """
    Remove old PAM modules that are no longer available in RHEL-8 from
    PAM configuration to avoid system lock out.
    """

    name = 'removed_pam_modules_apply'
    consumes = (RemovedPAMModules,)
    produces = ()
    tags = (IPUWorkflowTag, PreparationPhaseTag)

    def process(self):
        for model in self.consume(RemovedPAMModules):
            for path in PAM.files:
                content = read_file(path)
                if not content:  # Nothing to do if no content?
                    continue

                with open(path, 'w') as f:
                    f.write(comment_modules(model.modules, content))
            break


# repos/system_upgrade/el7toel8/actors/removeoldpammodulescheck/actor.py

from leapp.actors import Actor
from leapp.dialogs import Dialog
from leapp.dialogs.components import BooleanComponent
from leapp.models import RemovedPAMModules
from leapp.reporting import Report, create_report
from leapp import reporting
from leapp.tags import IPUWorkflowTag, ChecksPhaseTag


class RemoveOldPAMModulesCheck(Actor):
    """
    Check if it is all right to disable PAM modules that are not in RHEL-8.

    If admin will refuse to disable these modules (pam_pkcs11 and pam_krb5),
    upgrade will be stopped. Otherwise we would risk locking out the system
    once these modules are removed.
    """
    name = 'removed_pam_modules_check'
    consumes = (RemovedPAMModules,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)
    dialogs = (
        Dialog(
            scope='remove_pam_pkcs11_module_check',
            reason='Confirmation',
            components=(
                BooleanComponent(
                    key='confirm',
                    label='Disable pam_pkcs11 module in PAM configuration? '
                          'If no, the upgrade process will be interrupted.',
                    description='PAM module pam_pkcs11 is no longer available '
                                'in RHEL-8 since it was replaced by SSSD.',
                    reason='Leaving this module in PAM configuration may '
                           'lock out the system.'
                ),
            )
        ),
        Dialog(
            scope='remove_pam_krb5_module_check',
            reason='Confirmation',
            components=(
                BooleanComponent(
                    key='confirm',
                    label='Disable pam_krb5 module in PAM configuration? '
                          'If no, the upgrade process will be interrupted.',
                    description='PAM module pam_krb5 is no longer available '
                                'in RHEL-8 since it was replaced by SSSD.',
                    reason='Leaving this module in PAM configuration may '
                           'lock out the system.'
                ),
            )
        ),
    )

    modules = []

    def process(self):
        model = next(self.consume(RemovedPAMModules))

        for module in model.modules:
            result = self.confirm(module)
            if result:
                self.produce_report(module)
            elif result is False:
                # user specifically chose to disagree with auto disablement
                self.produce_inhibitor(module)

    def confirm(self, module):
        questions = {
            'pam_pkcs11': self.dialogs[0],
            'pam_krb5': self.dialogs[1]
        }

        return self.get_answers(questions[module]).get('confirm')

    def produce_report(self, module):
        create_report([
            reporting.Title('Module {0} will be removed from PAM configuration'.format(module)),
            reporting.Summary(
                'Module {0} was surpassed by SSSD and therefore it was '
                'removed from RHEL-8. Keeping it in PAM configuration may '
                'lock out the system thus it will be automatically removed '
                'from PAM configuration before upgrading to RHEL-8. '
                'Please switch to SSSD to recover the functionality '
                'of {0}.'.format(module)
            ),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.TOOLS
            ]),
            reporting.Remediation(hint='Configure SSSD to replace {0}'.format(module)),
            reporting.RelatedResource('package', 'sssd')
        ])

    def produce_inhibitor(self, module):
        create_report([
            reporting.Title(
                'Upgrade process was interrupted because {0} is enabled in '
                'PAM configuration and SA user refused to disable it '
                'automatically.'.format(module)),
            reporting.Summary(
                'Module {0} was surpassed by SSSD and therefore it was '
                'removed from RHEL-8. Keeping it in PAM configuration may '
                'lock out the system thus it is necessary to disable it '
                'before the upgrade process can continue.'.format(module)
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Tags([
                    reporting.Tags.AUTHENTICATION,
                    reporting.Tags.SECURITY,
                    reporting.Tags.TOOLS
            ]),
            reporting.Flags([reporting.Flags.INHIBITOR]),
            reporting.Remediation(
                hint='Disable {0} module and switch to SSSD to recover its functionality.'.format(module)),
            reporting.RelatedResource('package', 'sssd')
        ])


# repos/system_upgrade/el7toel8/actors/removeoldpammodulesscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.removeoldpammodulesscanner import RemoveOldPAMModulesScannerLibrary
from leapp.libraries.common.pam import PAM
from leapp.models import RemovedPAMModules
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class RemoveOldPAMModulesScanner(Actor):
    """
    Scan PAM configuration for modules that are not available in RHEL-8.

    PAM module pam_krb5 and pam_pkcs11 are no longer present in RHEL-8
    and must be removed from PAM configuration, otherwise it may lock out
    the system.
    """
    name = 'removed_pam_modules_scanner'
    consumes = ()
    produces = (RemovedPAMModules,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        pam = PAM.from_system_configuration()
        scanner = RemoveOldPAMModulesScannerLibrary(pam)
        self.produce(scanner.process())


# repos/system_upgrade/el7toel8/actors/repositoriesblacklist/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import repositoriesblacklist
from leapp.models import (
    CustomTargetRepository,
    RepositoriesBlacklisted,
    RepositoriesFacts,
    RepositoriesMapping,
)
from leapp.reporting import Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class RepositoriesBlacklist(Actor):
    """
    Exclude target repositories provided by Red Hat without support.

    Conditions to exclude:
    - there are not such repositories already enabled on the source system
      (e.g. "Optional" repositories)
    - such repositories are not required for the upgrade explicitly by the user
      (e.g. via the --enablerepo option or via the /etc/leapp/files/leapp_upgrade_repositories.repo file)

    E.g. CRB repository is provided by Red Hat but it is without the support.
    """

    name = "repositories_blacklist"
    consumes = (
        CustomTargetRepository,
        RepositoriesFacts,
        RepositoriesMapping,
    )
    produces = (RepositoriesBlacklisted, Report)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        repositoriesblacklist.process()


# repos/system_upgrade/el7toel8/actors/sanebackendsmigrate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import sanebackendsmigrate
from leapp.models import InstalledRedHatSignedRPM
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class SanebackendsMigrate(Actor):
    """
    Actor for migrating sane-backends configuration files.

    Adds USB quirks for support specific USB scanners if they
    are not added during package manager transaction.
    """

    name = 'sanebackends_migrate'
    consumes = (InstalledRedHatSignedRPM,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        sanebackendsmigrate.update_sane()


# repos/system_upgrade/el7toel8/actors/sctpchecks/actor.py

from leapp.actors import Actor
from leapp.models import SCTPConfig, RpmTransactionTasks
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class SCTPChecks(Actor):
    """
    Parses collected SCTP information and take necessary actions.

    The only action performed by this actor is to request the installation of the
    kernel-modules-extra rpm package, based on if SCTP is being used or not which
    is collected on SCTPConfig message. If yes, it then produces a RpmTransactionTasks
    requesting to install the package.
    """
    name = 'sctp_checks'
    consumes = (SCTPConfig,)
    produces = (RpmTransactionTasks, )
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        for sctpconfig in self.consume(SCTPConfig):
            if sctpconfig.wanted:
                self.produce(RpmTransactionTasks(to_install=['kernel-modules-extra']))
                break


# repos/system_upgrade/el7toel8/actors/sctpconfigread/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.sctplib import is_sctp_wanted
from leapp.models import ActiveKernelModulesFacts, SCTPConfig
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class SCTPConfigRead(Actor):
    """
    Determines whether or not the SCTP kernel module might be wanted.

    This actor determines whether or not the SCTP is currently used by this machine or has been quite
    recently used (1 month timeframe). In case it has been used it will issue a SCTPConfig message that
    defines the decision whether or not the SCTP module should be removed from the module blacklist on RHEL8.
    """
    name = 'sctp_read_status'
    consumes = (ActiveKernelModulesFacts,)
    produces = (SCTPConfig,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        self.produce(SCTPConfig(wanted=is_sctp_wanted()))


# repos/system_upgrade/el7toel8/actors/sctpconfigupdate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import sctpupdate
from leapp.models import SCTPConfig
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class SCTPConfigUpdate(Actor):
    """
    Updates the kernel module blacklist for SCTP.

    If the SCTP module is wanted on RHEL8 the modprobe configuration gets updated to remove SCTP from the black listed
    kernel modules.
    """
    name = 'sctp_config_update'
    description = 'This actor updates SCTP configuration for RHEL8.'
    consumes = (SCTPConfig,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        sctpupdate.perform_update()


# repos/system_upgrade/el7toel8/actors/sourcebootloaderscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.sourcebootloaderscanner import scan_source_boot_loader_configuration
from leapp.models import SourceBootLoaderConfiguration
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class SourceBootLoaderScanner(Actor):
    """
    Scans the boot loader configuration on the source system.
    """

    name = 'source_boot_loader_scanner'
    consumes = ()
    produces = (SourceBootLoaderConfiguration,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        scan_source_boot_loader_configuration()


# repos/system_upgrade/el7toel8/actors/spamassassinconfigcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import spamassassinconfigcheck
from leapp.models import Report, SpamassassinFacts
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class SpamassassinConfigCheck(Actor):
    """
    Reports changes in spamassassin between RHEL-7 and RHEL-8

    Reports backward-incompatible changes that have been made in spamassassin
    between RHEL-7 and RHEL-8 (spamc no longer accepts an argument with the --ssl
    option; spamd no longer accepts the --ssl-version; SSLv3 is no longer supported;
    the type of spamassassin.service has been changed from "forking" to "simple";
    sa-update no longer supports SHA1 validation of rule files).

    The migration of the configuration files will be mostly handled by the
    SpamassassinConfigUpdate actor, however the admin still needs to know about
    the changes so that they can do any necessary migration in places that we cannot
    reach (e.g. scripts).
    """

    name = 'spamassassin_config_check'
    consumes = (SpamassassinFacts,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        facts = next(self.consume(SpamassassinFacts), None)
        if facts:
            spamassassinconfigcheck.produce_reports(facts)
        else:
            self.log.debug('Skipping execution - no SpamassassinFacts message has been produced.')


# repos/system_upgrade/el7toel8/actors/spamassassinconfigread/actor.py

import os

from leapp.actors import Actor
from leapp.libraries.actor import spamassassinconfigread
from leapp.libraries.common.utils import read_file
from leapp.models import InstalledRedHatSignedRPM, SpamassassinFacts
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class SpamassassinConfigRead(Actor):
    """
    Reads spamc configuration (/etc/mail/spamassassin/spamc.conf), the
    spamassassin sysconfig file (/etc/sysconfig/spamassassin) and checks
    whether the spamassassin service has been overriden. Produces
    SpamassassinFacts containing the extracted information.
    """

    name = 'spamassassin_config_read'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (SpamassassinFacts,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        if spamassassinconfigread.is_processable():
            self.produce(spamassassinconfigread.get_spamassassin_facts(read_func=read_file,
                                                                       listdir=os.listdir))


# repos/system_upgrade/el7toel8/actors/spamassassinconfigupdate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import spamassassinconfigupdate
from leapp.models import SpamassassinFacts
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class SpamassassinConfigUpdate(Actor):
    """
    This actor performs several modifications to spamassassin configuration
    so that spamc and the spamassassin systemd service can be run without error
    on the target system:
    1. Remove arguments given to the --ssl option in spamc configuration
       (/etc/mail/spamassassin/spamc.conf).
    2. Remove --ssl-version options from the spamassassin sysconfig file
       (/etc/sysconfig/spamassassin), or replace them with --ssl, if needed.
    3. Remove the -d/--daemonize option from the spamassassin sysconfig file.

    All files are backed up before they are modified.
    """

    name = 'spamassassin_config_update'
    consumes = (SpamassassinFacts,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        facts = next(self.consume(SpamassassinFacts), None)
        if facts:
            spamassassinconfigupdate.migrate_configs(facts)
        else:
            self.log.debug('Skipping execution - no SpamassassinFacts message has been produced.')


# repos/system_upgrade/el7toel8/actors/sssdcheck/actor.py

from leapp.actors import Actor
from leapp.models import SSSDConfig
from leapp import reporting
from leapp.reporting import Report, create_report
from leapp.tags import IPUWorkflowTag, ChecksPhaseTag


COMMON_REPORT_TAGS = [reporting.Tags.AUTHENTICATION, reporting.Tags.SECURITY]

related = [
    reporting.RelatedResource('package', 'sssd'),
    reporting.RelatedResource('file', '/etc/sssd/sssd.conf')
]


class SSSDCheck(Actor):
    """
    Check SSSD configuration for changes in RHEL8 and report them.

    These changes are:
    - id_provider=local is no longer supported and will be ignored
    - ldap_groups_use_matching_rule_in_chain was removed and will be ignored
    - ldap_initgroups_use_matching_rule_in_chain was removed and will be ignored
    - ldap_sudo_include_regexp changed default from true to false
    """

    name = 'sssd_check'
    consumes = (SSSDConfig,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        model = next(self.consume(SSSDConfig), None)
        if not model:
            return

        for domain in model.domains:
            if 'local_provider' in domain.options:
                self.reportLocalProvider(domain)

            if 'groups_chain' in domain.options:
                self.reportRemovedOption(domain, 'ldap_groups_use_matching_rule_in_chain')

            if 'initgroups_chain' in domain.options:
                self.reportRemovedOption(domain, 'ldap_initgroups_use_matching_rule_in_chain')

            if 'sudo_regexp' in domain.options:
                self.reportSudoRegexp(domain)

    def reportLocalProvider(self, domain):
        create_report([
            reporting.Title('SSSD Domain "%s": local provider is no longer '
                            'supported and the domain will be ignored.' % domain),
            reporting.Summary('Local provider is no longer supported.'),
            reporting.Tags(COMMON_REPORT_TAGS),
            reporting.Severity(reporting.Severity.MEDIUM)
        ] + related)

    def reportRemovedOption(self, domain, option):
        create_report([
            reporting.Title('SSSD Domain "%s": option %s has no longer '
                            'any effect' % (domain, option)),
            reporting.Summary('Option %s was removed and it will be ignored.' % option),
            reporting.Tags(COMMON_REPORT_TAGS),
            reporting.Severity(reporting.Severity.MEDIUM)
        ] + related)

    def reportSudoRegexp(self, domain):
        create_report([
            reporting.Title('SSSD Domain "%s": sudo rules containing wildcards '
                            'will stop working.' % domain),
            reporting.Summary('Default value of ldap_sudo_include_regexp changed '
                              'from true to false for performance reason.'),
            reporting.Tags(COMMON_REPORT_TAGS),
            reporting.Remediation(
                hint='If you use sudo rules with wildcards, set this option to true explicitly.'
            ),
            reporting.Severity(reporting.Severity.HIGH)
        ] + related)


# repos/system_upgrade/el7toel8/actors/sssdfacts/actor.py

from six.moves import configparser

from leapp.actors import Actor
from leapp.libraries.actor.sssdfacts import SSSDFactsLibrary
from leapp.models import SSSDConfig
from leapp.tags import IPUWorkflowTag, FactsPhaseTag


class SSSDFacts(Actor):
    """
    Check SSSD configuration for changes in RHEL8 and report them in model.

    These changes are:
    - id_provider=local is no longer supported and will be ignored
    - ldap_groups_use_matching_rule_in_chain was removed and will be ignored
    - ldap_initgroups_use_matching_rule_in_chain was removed and will be ignored
    - ldap_sudo_include_regexp changed default from true to false
    """

    name = 'sssd_facts'
    consumes = ()
    produces = (SSSDConfig,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        try:
            config = configparser.RawConfigParser()
            config.read('/etc/sssd/sssd.conf')
        except configparser.Error:
            # SSSD is not configured properly. Nothing to do.
            self.log.warning('SSSD configuration unreadable.')
            return

        self.produce(SSSDFactsLibrary(config).process())


# repos/system_upgrade/el7toel8/actors/tcpwrapperscheck/actor.py

from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.models import Report, TcpWrappersFacts, InstalledRedHatSignedRPM
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp.libraries.stdlib import api
from leapp.libraries.actor.tcpwrapperscheck import config_affects_daemons
from leapp.libraries.common.rpms import create_lookup
from leapp.reporting import create_report
from leapp import reporting

DAEMONS = [
    ("audit", ["auditd"]),
    ("bacula", ["bacula"]),
    ("conserver", ["conserver"]),
    ("cyrus-imapd", ["imap", "pop3", "imaps", "pop3s", "sieve", "lmtp"]),
    ("dovecot", ["imap", "pop3", "imaps", "pop3s", "sieve", "lmtp"]),
    ("nfs-utils", ["mountd", "statd"]),
    ("openssh-server", ["sshd"]),
    ("proftpd", ["proftpd"]),
    ("quota", ["rquotad"]),
    ("rpcbind", ["rpcbind"]),
    # ("sendmail", ["sendmail"]),  # Handled in CheckSendmail Actor
    ("slapi-nis", ["slapi-nis"]),
    ("socat", ["socat"]),
    ("stunnel", ["stunnel"]),
    ("tftp-server", ["tftpd"]),
    # ("vsftpd", ["vsftpd"]),  # Handled in VsftpdConfigCheck Actor
    ("xinetd", ["xinetd"]),
]


class TcpWrappersCheck(Actor):
    """
    Check the list of packages previously compiled with TCP wrappers support
    and check whether they have some rules configured in
    /etc/hosts.{allow,deny} that are no longer honored by the daemons and
    might make the service more accessible than expected.
    """

    name = 'tcp_wrappers_check'
    consumes = (TcpWrappersFacts, InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        # Consume a single TCP Wrappers message
        tcp_wrappers_messages = self.consume(TcpWrappersFacts)
        tcp_wrappers_facts = next(tcp_wrappers_messages, None)
        if list(tcp_wrappers_messages):
            api.current_logger().warning('Unexpectedly received more than one TcpWrappersFacts message.')
        if not tcp_wrappers_facts:
            raise StopActorExecutionError(
                'Could not check tcp wrappers configuration', details={'details': 'No TcpWrappersFacts found.'}
            )

        # Convert installed packages message to list
        packages = create_lookup(InstalledRedHatSignedRPM, field='items', keys=('name',))

        found_packages = config_affects_daemons(tcp_wrappers_facts, packages, DAEMONS)

        if found_packages:
            create_report([
                reporting.Title('TCP Wrappers configuration affects some installed packages'),
                reporting.Summary(
                    'tcp_wrappers support has been removed in RHEL-8. '
                    'There is some configuration affecting installed packages (namely {}) '
                    'in /etc/hosts.deny or /etc/hosts.allow, which '
                    'is no longer going to be effective after update. '
                    'Please migrate it manually.'.format(', '.join(found_packages))
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.ExternalLink(
                    title='Replacing TCP Wrappers in RHEL 8',
                    url='https://access.redhat.com/solutions/3906701'
                ),
                reporting.Tags([reporting.Tags.SECURITY, reporting.Tags.NETWORK]),
                reporting.Flags([reporting.Flags.INHIBITOR]),
                reporting.RelatedResource('file', '/etc/hosts.allow'),
                reporting.RelatedResource('file', '/etc/hosts.deny'),
                reporting.RelatedResource('package', 'tcp_wrappers')
            ] + [reporting.RelatedResource('package', fp) for fp in found_packages])


# repos/system_upgrade/el7toel8/actors/tcpwrappersconfigread/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import tcpwrappersconfigread
from leapp.models import TcpWrappersFacts
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class TcpWrappersConfigRead(Actor):
    """
    Parse tcp_wrappers configuration files /etc/hosts.{allow,deny}.
    """

    name = 'tcp_wrappers_config_read'
    consumes = ()
    produces = (TcpWrappersFacts,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        self.produce(tcpwrappersconfigread.get_tcp_wrappers_facts())


# repos/system_upgrade/el7toel8/actors/updateetcsysconfigkernel/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import updateetcsysconfigkernel
from leapp.tags import PreparationPhaseTag, IPUWorkflowTag


class UpdateEtcSysconfigKernel(Actor):
    """
    Update /etc/sysconfig/kernel file.

    In order to proceed with Upgrade process, DEFAULTKERNEL entry should be updated from kernel to
    kernel-core.
    """

    name = 'update_etc_sysconfig_kernel'
    consumes = ()
    produces = ()
    tags = (PreparationPhaseTag, IPUWorkflowTag)

    def process(self):
        updateetcsysconfigkernel.update_kernel_config('/etc/sysconfig/kernel')


# repos/system_upgrade/el7toel8/actors/vimmigrate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import vimmigrate
from leapp.models import InstalledRedHatSignedRPM
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class VimMigrate(Actor):
    """
    Modify configuration files of Vim 8.0 and later to keep the same behavior
    as Vim 7.4 and earlier had.
    """

    name = 'vim_migrate'
    consumes = (InstalledRedHatSignedRPM,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        vimmigrate.update_vim()


# repos/system_upgrade/el7toel8/actors/vsftpdconfigcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import vsftpdconfigcheck
from leapp.models import TcpWrappersFacts, VsftpdFacts
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class VsftpdConfigCheck(Actor):
    """
    Checks whether the vsftpd configuration is supported in RHEL-8. Namely checks that
    configuration files don't set tcp_wrappers=YES and vsftpd-related configuration is
    not present in tcp_wrappers configuration files at the same time.
    """

    name = 'vsftpd_config_check'
    consumes = (TcpWrappersFacts, VsftpdFacts,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag,)

    def process(self):
        try:
            vsftpd_facts = next(self.consume(VsftpdFacts))
        except StopIteration:
            return
        tcp_wrappers_facts = next(self.consume(TcpWrappersFacts))
        vsftpdconfigcheck.check_config_supported(tcp_wrappers_facts, vsftpd_facts)


# repos/system_upgrade/el7toel8/actors/vsftpdconfigread/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import vsftpdconfigread
from leapp.models import InstalledRedHatSignedRPM, VsftpdFacts
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class VsftpdConfigRead(Actor):
    """
    Reads vsftpd configuration files (/etc/vsftpd/*.conf) and extracts necessary information.
    """

    name = 'vsftpd_config_read'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (VsftpdFacts,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        installed_rpm_facts = next(self.consume(InstalledRedHatSignedRPM))
        if vsftpdconfigread.is_processable(installed_rpm_facts):
            self.produce(vsftpdconfigread.get_vsftpd_facts())


# repos/system_upgrade/el7toel8/actors/vsftpdconfigupdate/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import vsftpdconfigupdate
from leapp.models import VsftpdFacts
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class VsftpdConfigUpdate(Actor):
    """
    Modifies vsftpd configuration files on the target RHEL-8 system so that the effective
    configuration is the same, where possible. This means doing two things:
    1. Reverting the default configuration file (/etc/vsftpd/vsftpd.conf) to its state
       before the upgrade (where it makes sense), if the configuration file was being used
       with its default content (i.e., unmodified) on the source system (the configuration
       file gets replaced with a new version during the RPM upgrade in this case).
       The anonymous_enable option falls in this category.
    2. Adding 'option=old_effective_value' to configuration files for options whose default
       value has changed, if the option is not explicitly specified in the configuration file.
       The strict_ssl_read_eof option falls in this category.
    3. Disabling options that cannot be enabled, otherwise vsftpd wouldn't work.
       The tcp_wrappers option falls in this category.
    """

    name = 'vsftpd_config_update'
    consumes = (VsftpdFacts,)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        try:
            vsftpd_facts = next(self.consume(VsftpdFacts))
        except StopIteration:
            return
        vsftpdconfigupdate.migrate_configs(vsftpd_facts)


# repos/system_upgrade/el7toel8/actors/ziplcheckbootentries/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.ziplcheckbootentries import inhibit_if_invalid_zipl_configuration
from leapp.models import SourceBootLoaderConfiguration
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class ZiplCheckBootEntries(Actor):
    """
    Inhibits the upgrade if a problematic Zipl configuration is detected on the system.

    The configuration is considered problematic if it will cause troubles during its conversion to BLS.
    Such troubles can be caused by either containing multiple rescue entries, or containing rescue entries
    sharing the same kernel image version.
    """

    name = 'zipl_check_boot_entries'
    consumes = (SourceBootLoaderConfiguration,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        boot_loader_configuration = next(self.consume(SourceBootLoaderConfiguration))
        inhibit_if_invalid_zipl_configuration(boot_loader_configuration)


# repos/system_upgrade/el7toel8/actors/ziplconverttoblscfg/actor.py

import os
import filecmp

from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import mounting
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import CalledProcessError
from leapp.models import TargetUserSpaceInfo
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class ZiplConvertToBLSCFG(Actor):
    """
    Convert the zipl boot loader configuration to the the boot loader specification on s390x systems.
    """

    name = 'zipl_convert_to_blscfg'
    consumes = (TargetUserSpaceInfo,)
    produces = ()
    tags = (IPUWorkflowTag, PreparationPhaseTag)

    def process(self):
        if not architecture.matches_architecture(architecture.ARCH_S390X):
            return
        userspace = next(self.consume(TargetUserSpaceInfo), None)
        if not userspace:
            # actually this should not happen, but in such case, we want to still
            # rather continue even if we boot into the old kernel, but in such
            # case, people will have to do manual actions.
            # NOTE: it is really just hypothetical
            self.log_error(
                'TargetUserSpaceInfo is missing. Cannot execute zipl-switch-to-blscfg'
                ' to convert the zipl configuration to BLS.'
            )
            raise StopActorExecutionError('GENERAL FAILURE: Input data for the actor are missing.')

        # replace the original boot directory inside the container by the host one
        # - as we cannot use zipl* pointing anywhere else than default directory
        # - no, --bls-directory is not solution
        with mounting.BindMount(source='/boot', target=os.path.join(userspace.path, 'boot')):
            userspace_zipl_conf = os.path.join(userspace.path, 'etc', 'zipl.conf')
            if os.path.exists(userspace_zipl_conf):
                os.remove(userspace_zipl_conf)
            with mounting.NullMount(target=userspace.path) as userspace:
                with userspace.nspawn() as context:
                    context.copy_to('/etc/zipl.conf', '/etc/zipl.conf')
                    # zipl needs this one as well
                    context.copy_to('/etc/machine-id', '/etc/machine-id')
                    try:
                        context.call(['/usr/sbin/zipl-switch-to-blscfg'])
                        if filecmp.cmp('/etc/zipl.conf', userspace_zipl_conf):
                            # When the files are same, zipl failed - see the switch script
                            raise OSError('Failed to convert the ZIPL configuration to BLS.')
                        context.copy_from('/etc/zipl.conf', '/etc/zipl.conf')
                    except OSError as e:
                        self.log.error('Could not call zipl-switch-to-blscfg command.',
                                       exc_info=True)
                        raise StopActorExecutionError(
                            message='Failed to execute zipl-switch-to-blscfg.',
                            details={'details': str(e)}
                        )
                    except CalledProcessError as e:
                        self.log.error('zipl-switch-to-blscfg execution failed,',
                                       exc_info=True)
                        raise StopActorExecutionError(
                            message='zipl-switch-to-blscfg execution failed with non zero exit code.',
                            details={'details': str(e), 'stdout': e.stdout, 'stderr': e.stderr}
                        )

                        # FIXME: we do not want to continue anymore, but we should clean
                        # better.
                        # NOTE: Basically, just removal of the /boot/loader dir content inside
                        # could be enough, but we cannot remove /boot/loader because of boom
                        # - - if we remove it, we will remove the snapshot as well
                        # - - on the other hand, we should't keep it there if zipl
                        # - - has not been converted to BLS


# repos/system_upgrade/el7toel8/models/authselect.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic, SystemInfoTopic


class Authselect(Model):
    """
    Suggested changes that will convert the system to authselect.

    This model describes the authselect call that can be used to convert
    existing configuration into a equivalent or similar configuration
    that is generated by authselect.
    """
    topic = SystemFactsTopic

    profile = fields.Nullable(fields.String(default=None))
    """
    Suggested authselect profile name.
    """

    features = fields.List(fields.String())
    """
    Suggested authselect profile features.
    """

    confirm = fields.Boolean(default=True)
    """
    Changes to the system requires admin confirmation.
    """


class AuthselectDecision(Model):
    """
    Confirmation of changes suggested in Authselect model.

    If confirmed is True, the changes will be applied on RHEL-8 machine.
    """
    topic = SystemInfoTopic

    confirmed = fields.Boolean(default=False)
    """
    If true, authselect should be called after upgrade.
    """


# repos/system_upgrade/el7toel8/models/bootloaderconfiguration.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class BootEntry(Model):
    """
    One entry in the boot loader configuration.

    Not meant to be produced directly, only as a part of :class:`SourceBootLoaderConfiguration`.
    """
    topic = SystemInfoTopic

    title = fields.String()
    """Title of the boot entry."""

    kernel_image = fields.String()
    """Kernel image of the boot entry."""


class SourceBootLoaderConfiguration(Model):
    """Describes the bootloader configuration found on the source system."""
    topic = SystemInfoTopic

    entries = fields.List(fields.Model(BootEntry))
    """Boot entries available in the bootloader configuration."""


# repos/system_upgrade/el7toel8/models/brlttymigrationdecision.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class BrlttyMigrationDecision(Model):
    topic = SystemInfoTopic
    migrate_file = fields.String()
    migrate_bt = fields.Boolean()
    migrate_espeak = fields.Boolean()


# repos/system_upgrade/el7toel8/models/cupschangedfeatures.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class CupsChangedFeatures(Model):
    topic = SystemInfoTopic

    interface = fields.Boolean(default=False)
    """
    True if interface scripts are used, False otherwise
    """

    digest = fields.Boolean(default=False)
    """
    True if Digest/BasicDigest directive values are used, False otherwise
    """

    include = fields.Boolean(default=False)
    """
    True if Include directive is used, False otherwise
    """

    certkey = fields.Boolean(default=False)
    """
    True if ServerKey/ServerCertificate directives are used, False otherwise
    """

    env = fields.Boolean(default=False)
    """
    True if PassEnv/SetEnv directives are used, False otherwise
    """

    printcap = fields.Boolean(default=False)
    """
    True if PrintcapFormat directive is used, False otherwise
    """

    include_files = fields.List(fields.String(), default=['/etc/cups/cupsd.conf'])
    """
    Paths to included files, contains /etc/cups/cupsd.conf by default
    """


# repos/system_upgrade/el7toel8/models/firewalldfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class FirewalldFacts(Model):
    """The model contains firewalld configuration."""
    topic = SystemInfoTopic

    firewall_config_command = fields.String(default='')
    ebtablesTablesInUse = fields.List(fields.String(), default=[])
    ipsetTypesInUse = fields.List(fields.String(), default=[])


# repos/system_upgrade/el7toel8/models/installedkdeappsfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class InstalledKdeAppsFacts(Model):
    topic = SystemFactsTopic
    installed_apps = fields.List(fields.String(), default=[])


# repos/system_upgrade/el7toel8/models/networkmanagerconfig.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class NetworkManagerConfig(Model):
    """The model contains NetworkManager configuration."""
    topic = SystemInfoTopic
    dhcp = fields.String(default='')


# repos/system_upgrade/el7toel8/models/ntpmigrationdecision.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class NtpMigrationDecision(Model):
    topic = SystemInfoTopic
    migrate_services = fields.List(fields.String())
    config_tgz64 = fields.String()


# repos/system_upgrade/el7toel8/models/opensshconfig.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class OpenSshPermitRootLogin(Model):
    topic = SystemInfoTopic

    value = fields.StringEnum(['yes', 'prohibit-password',
                               'forced-commands-only', 'no'])
    """ Value of a PermitRootLogin directive. """
    in_match = fields.Nullable(fields.List(fields.String()))
    """ Criteria of Match blocks the PermitRootLogin directive occured in, if any. """


class OpenSshConfig(Model):
    """
    OpenSSH server configuration.

    This model contains the first effective configuration option specified
    in the configuration file or a list of all the options specified
    in all the conditional blocks used throughout the file.
    """
    topic = SystemInfoTopic

    permit_root_login = fields.List(fields.Model(OpenSshPermitRootLogin))
    """ All PermitRootLogin directives. """
    use_privilege_separation = fields.Nullable(fields.StringEnum(['sandbox',
                                                                  'yes',
                                                                  'no']))
    """ Value of the UsePrivilegeSeparation directive, if present. Removed in RHEL 8. """
    protocol = fields.Nullable(fields.String())
    """ Value of the Protocols directive, if present. Removed in RHEL 8. """
    ciphers = fields.Nullable(fields.String())
    """ Value of the Ciphers directive, if present. Ciphers separated by comma. """
    macs = fields.Nullable(fields.String())
    """ Value of the MACs directive, if present. """
    modified = fields.Boolean(default=False)
    """ True if the configuration file was modified. """
    deprecated_directives = fields.List(fields.String())
    """ Configuration directives that were deprecated in the new version of openssh. """


# repos/system_upgrade/el7toel8/models/pamconfiguration.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class PamService(Model):
    """
    Pam service description

    This model contains information about pam modules used by specific PAM
    service/filename
    """
    topic = SystemInfoTopic

    service = fields.String()
    modules = fields.List(fields.String())
    # Should this also list includes?


class PamConfiguration(Model):
    """
    Global PAM configuration

    This model describes separate services using PAM and what pam modules are
    used in each of them. Consumer can select just the pam services he is
    interested in or scan for specific configuration throughout all the services.
    """
    topic = SystemInfoTopic

    services = fields.List(fields.Model(PamService))


# repos/system_upgrade/el7toel8/models/quaggatofrrfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class QuaggaToFrrFacts(Model):
    """
    Model for quagga to frr actors.

    A list of configuration files used by quagga. This list is used to add yes/no to
    /etc/frr/daemons file. It indicates which daemons from frr should be run.
    """
    topic = SystemInfoTopic

    active_daemons = fields.List(fields.String())
    enabled_daemons = fields.List(fields.String())


# repos/system_upgrade/el7toel8/models/removedpammodules.py

from leapp.models import Model, fields
from leapp.topics import SystemFactsTopic


class RemovedPAMModules(Model):
    """
    PAM modules that were removed from RHEL8 but are in current configuration.
    """
    topic = SystemFactsTopic

    modules = fields.List(fields.String())
    """
    List of PAM modules that were detected in current configuration but
    are no longer available in RHEL8.
    """


# repos/system_upgrade/el7toel8/models/sctpconfigmodel.py

from leapp.models import Model, fields
from leapp.topics import SCTPConfigTopic


class SCTPConfig(Model):
    topic = SCTPConfigTopic
    wanted = fields.Boolean(default=False)


# repos/system_upgrade/el7toel8/models/sendmailmigrationdecision.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class SendmailMigrationDecision(Model):
    topic = SystemInfoTopic
    migrate_files = fields.List(fields.String())


# repos/system_upgrade/el7toel8/models/spamassassinfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class SpamassassinFacts(Model):
    topic = SystemInfoTopic

    spamc_ssl_argument = fields.Nullable(fields.String())
    """
    SSL version specified by the --ssl option in the spamc config file, or None
    if no value is given.
    """

    spamd_ssl_version = fields.Nullable(fields.String())
    """
    SSL version specified by the --ssl-version in the spamassassin sysconfig file,
    or None if no value is given.
    """

    service_overriden = fields.Boolean()
    """
    True if spamassassin.service is overriden, else False.
    """


# repos/system_upgrade/el7toel8/models/sssd.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class SSSDDomainConfig(Model):
    """
    Facts found about an SSSD domain.
    """
    topic = SystemInfoTopic

    name = fields.String()
    """
    Domain name.
    """

    options = fields.List(fields.String(), default=list())
    """
    List of options related to this domain that affects the upgrade process.
    """


class SSSDConfig(Model):
    """
    List of SSSD domains and their configuration that is related to the
    upgrade process.
    """
    topic = SystemInfoTopic

    domains = fields.List(fields.Model(SSSDDomainConfig), default=list())
    """
    SSSD Domains configuration.
    """


# repos/system_upgrade/el7toel8/models/tcpwrappersfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class DaemonList(Model):
    """
    A split up representation of a daemon_list (see host_access(5)). Example value of the
    'value' attribute: ["ALL", "EXCEPT", "in.fingerd"]
    """
    topic = SystemInfoTopic

    value = fields.List(fields.String())


class TcpWrappersFacts(Model):
    """
    A representation of tcp_wrappers configuration. Currently it only contains a list
    of daemon lists that are present in the tcp_wrappers configuration files. From this
    you can extract information on whether there is any configuration that applies to
    a specific daemon (see leapp.libraries.common.tcpwrappersutils.config_applies_to_daemon()).
    """
    topic = SystemInfoTopic

    daemon_lists = fields.List(fields.Model(DaemonList))


# repos/system_upgrade/el7toel8/models/vsftpdfacts.py

from leapp.models import Model, fields
from leapp.topics import SystemInfoTopic


class VsftpdConfig(Model):
    """
    Model representing some aspects of a vsftpd configuration file.

    The attributes representing the state of configuration options are nullable, so that
    they can represent the real state of the option in the file: if an option is set to "YES"
    in the configuration file, the corresponding attribute is set to True; if the option
    is set to NO, the attribute is set to False; if the option is not present in the config
    file at all, the attribute is set to None.
    """
    topic = SystemInfoTopic

    path = fields.String()
    """Path to the vsftpd configuration file"""
    strict_ssl_read_eof = fields.Nullable(fields.Boolean())
    """Represents the state of the strict_ssl_read_eof option in the config file"""
    tcp_wrappers = fields.Nullable(fields.Boolean())
    """Represents the state of the tcp_wrappers option in the config file"""


class VsftpdFacts(Model):
    topic = SystemInfoTopic

    default_config_hash = fields.Nullable(fields.String())
    """SHA1 hash of the /etc/vsftpd/vsftpd.conf file, if it exists, None otherwise"""
    configs = fields.List(fields.Model(VsftpdConfig))
    """List of vsftpd configuration files"""


# repos/system_upgrade/el7toel8/libraries/pam.py

import os
import re


class PAM(object):
    files = [
        '/etc/pam.d/system-auth',
        '/etc/pam.d/smartcard-auth',
        '/etc/pam.d/password-auth',
        '/etc/pam.d/fingerprint-auth',
        '/etc/pam.d/postlogin'
    ]
    """
    List of system PAM configuration files.
    """

    def __init__(self, config):
        self.modules = self.parse(config)

    def parse(self, config):
        """
        Parse configuration and return list of modules that are present in the
        configuration.
        """
        result = re.findall(
            r"^[ \t]*[^#\s]+.*(pam_\S+)\.so.*$",
            config,
            re.MULTILINE
        )

        return result

    def has(self, module):
        """
        Return True if the module exist in the configuration, False otherwise.
        """
        return module in self.modules

    def has_unknown_module(self, known_modules):
        """
        Return True if the configuration has any module which is not known to
        the caller, False otherwise.
        """
        for module in self.modules:
            if module not in known_modules:
                return True

        return False

    @staticmethod
    def read_file(config):
        """
        Read file contents. Return empty string if the file does not exist.
        """
        if not os.path.isfile(config):
            return ""
        with open(config) as f:
            return f.read()

    @staticmethod
    def from_system_configuration():
        config = ""
        for f in PAM.files:
            config += PAM.read_file(f)

        return PAM(config)


# repos/system_upgrade/el7toel8/libraries/spamassassinutils.py

import re


SPAMC_CONFIG_FILE = '/etc/mail/spamassassin/spamc.conf'
SPAMASSASSIN_SERVICE_OVERRIDE = '/etc/systemd/system/spamassassin.service'
SYSCONFIG_SPAMASSASSIN = '/etc/sysconfig/spamassassin'
SYSCONFIG_VARIABLE = 'SPAMDOPTIONS'
SPAMD_SHORTOPTS_NOARG = "ch46LlxPQqVv"
""" All short options in spamd that do not accept an argument, excluding -d. """


def parse_sysconfig_spamassassin(content):
    """
    Splits up a spamassassin sysconfig file into three parts and returns those parts:
    1. Beginning of the file up to the SPAMDOPTIONS assignment
    2. The assignment to the SPAMDOPTIONS variable (this is the assignment
       that takes effect, i.e. the last assignment to the variable)
    3. End of the file after the SPAMDOPTIONS assignment
    """
    line_continues = False
    is_assignment = False
    assignment_start = None
    assignment_end = None
    lines = content.split('\n')
    for ix, line in enumerate(lines):
        is_assignment = ((is_assignment and line_continues) or
                         (not (not is_assignment and line_continues) and
                          re.match(r'\s*' + SYSCONFIG_VARIABLE + '=', line)))
        if is_assignment:
            if line_continues:
                assignment_end += 1
            else:
                assignment_start = ix
                assignment_end = ix + 1
        line_continues = line.endswith('\\')

    if assignment_start is None:
        return content, '', ''
    assignment = ''
    for line in lines[assignment_start:assignment_end - 1]:
        assignment += line[:-1]
    assignment += lines[assignment_end - 1]
    pre_assignment = '\n'.join(lines[:assignment_start])
    post_assignment = '\n'.join(lines[assignment_end:])
    return pre_assignment, assignment, post_assignment


# repos/system_upgrade/el7toel8/libraries/tcpwrappersutils.py

import re


def _build_regex(pattern):
    regex = '^'
    part_beginning = 0
    while part_beginning < len(pattern):
        ix1 = pattern.find('*', part_beginning)
        ix2 = pattern.find('?', part_beginning)
        ix1 = len(pattern) if ix1 < 0 else ix1
        ix2 = len(pattern) if ix2 < 0 else ix2
        part_end = min(ix1, ix2)

        regex += re.escape(pattern[part_beginning:part_end])

        if part_end < len(pattern):
            if pattern[part_end] == '*':
                regex += '.*'
            else:
                regex += '.'

        part_beginning = part_end + 1

    regex += '$'
    return regex


def _pattern_matches(pattern, string):
    if pattern.lower() == 'all':
        return True
    regex = _build_regex(pattern)
    return re.match(regex, string, re.IGNORECASE) is not None


def _daemon_list_matches_daemon(daemon_list, daemon, recursion_depth):
    try:
        cur_list_end = daemon_list.index('except')
    except ValueError:
        cur_list_end = len(daemon_list)
    cur_list = daemon_list[:cur_list_end]
    matches_cur_list = False
    for item in cur_list:
        try:
            ix = item.index('@')
            # For simplicity, we ignore the host part. So we must make sure
            # that a daemon list containing a host-based pattern will always match
            # the daemon part of that host-based pattern (e.g. 'all except vsftpd@localhost
            # matches 'vsftpd'). See test_config_applies_to_daemon_with_host_except().
            if recursion_depth % 2 == 1:
                continue
            pattern = item[:ix]
        except ValueError:
            pattern = item
        if _pattern_matches(pattern, daemon):
            matches_cur_list = True
            break

    next_list = daemon_list[cur_list_end + 1:]
    if not next_list:
        matches_next_list = False
    else:
        matches_next_list = _daemon_list_matches_daemon(next_list, daemon, recursion_depth + 1)

    return matches_cur_list and not matches_next_list


def config_applies_to_daemon(facts, daemon):
    """
    Returns True if the specified tcp_wrappers configuration applies to the specified daemon.
    Otherwise returns False.

    This information is intended to be used in the Checks phase to check whether there is
    any tcp_wrappers configuration that the user needs to migrate manually and whether we
    should inhibit the upgrade, so that the upgraded system is not insecure.

    :param facts: A TcpWrappersFacts representation of the tcp_wrappers configuration
    :param daemon: The daemon name
    """
    for daemon_list in facts.daemon_lists:
        value = [item.lower() for item in daemon_list.value]
        if _daemon_list_matches_daemon(value, daemon, 0):
            return True
    return False


# repos/system_upgrade/el7toel8/libraries/vsftpdutils.py

import errno
import hashlib

from leapp.libraries.stdlib import api

VSFTPD_CONFIG_DIR = '/etc/vsftpd'
VSFTPD_DEFAULT_CONFIG_PATH = '/etc/vsftpd/vsftpd.conf'
STRICT_SSL_READ_EOF = 'strict_ssl_read_eof'
TCP_WRAPPERS = 'tcp_wrappers'


def read_file(path):
    """
    Read a file in text mode and return the contents.

    :param path: File path
    """
    with open(path, 'r') as f:
        return f.read()


def get_config_contents(path, read_func=read_file):
    """
    Try to read a vsftpd configuration file

    Try to read a vsftpd configuration file, log a warning if an error happens.
    :param path: File path
    :param read_func: Function to use to read the file. This is meant to be overriden in tests.
    :return: File contents or None, if the file could not be read
    """
    try:
        return read_func(path)
    except IOError as e:
        if e.errno != errno.ENOENT:
            api.current_logger().warning('Failed to read vsftpd configuration file: %s' % e)
        return None


def get_default_config_hash(read_func=read_file):
    """
    Read the default vsftpd configuration file (/etc/vsftpd/vsftpd.conf) and return its hash.

    :param read_func: Function to use to read the file. This is meant to be overriden in tests.
    :return SHA1 hash of the configuration file, or None if the file could not be read.
    """
    content = get_config_contents(VSFTPD_DEFAULT_CONFIG_PATH, read_func=read_func)
    if content is None:
        return None
    content = content.encode(encoding='utf-8')
    h = hashlib.sha1(content)
    return h.hexdigest()


# repos/system_upgrade/el7toel8/libraries/tests/test_multipathutil.py

import leapp.libraries.common.multipathutil as lib


def test_no_config():
    content = lib.read_config('/this/does/not/exist')
    assert content is None


def test_blank_line():
    data = lib.LineData('', None, False)
    assert data.type == data.TYPE_BLANK
    data = lib.LineData('    ', 'devices', True)
    assert data.type == data.TYPE_BLANK
    data = lib.LineData('# comment at start of line', 'defaults', False)
    assert data.type == data.TYPE_BLANK
    data = lib.LineData('  ! alternate comment method!!!', 'blacklist', True)
    assert data.type == data.TYPE_BLANK
    data = lib.LineData('# } still a comment', None, False)
    assert data.type == data.TYPE_BLANK
    data = lib.LineData('   # "also a comment!"', "multipaths", True)
    assert data.type == data.TYPE_BLANK
    data = lib.LineData(' "ignore lines starting with a string"', None, False)
    assert data.type == data.TYPE_BLANK


def test_section_end():
    data = lib.LineData('}', None, False)
    assert data.type == data.TYPE_SECTION_END
    data = lib.LineData('     }  # trailing comment', 'devices', True)
    assert data.type == data.TYPE_SECTION_END
    data = lib.LineData('     }  JUNK AFTERWARDS', 'defaults', False)
    assert data.type == data.TYPE_SECTION_END
    data = lib.LineData('\t \t}  !!tabs', 'multipaths', True)
    assert data.type == data.TYPE_SECTION_END


def test_section_start():
    sections = ('defaults', 'blacklist', 'blacklist_exceptions', 'devices',
                'overrides', 'multipaths')
    subsections = {'blacklist': 'device', 'blacklist_exceptions': 'device',
                   'devices': 'device', 'multipaths': 'multipath'}
    for section in sections:
        data = lib.LineData(section + ' {', None, False)
        assert data.type == data.TYPE_SECTION_START
        assert data.section == section

    try:
        data = lib.LineData('not_a_section {', None, False)
        assert False
    except ValueError:
        pass

    data = lib.LineData('defaults # works even without brace', None, False)
    assert data.type == data.TYPE_SECTION_START
    assert data.section == 'defaults'

    data = lib.LineData('\t \tmultipaths { # tabs', None, False)
    assert data.type == data.TYPE_SECTION_START
    assert data.section == 'multipaths'

    data = lib.LineData('devices{ # do not need a space', None, False)
    assert data.type == data.TYPE_SECTION_START
    assert data.section == 'devices'

    for section in subsections:
        data = lib.LineData(subsections[section] + ' {', section, False)
        assert data.type == data.TYPE_SECTION_START
        assert data.section == subsections[section]

    data = lib.LineData('devices { # wrong section', 'multipath', False)
    assert data.type != data.TYPE_SECTION_START

    data = lib.LineData('devices { # already in subsection', 'device', True)
    assert data.type != data.TYPE_SECTION_START


def test_option():
    data = lib.LineData('key value', 'defaults', False)
    assert data.type == data.TYPE_OPTION
    assert data.option == 'key'
    assert data.value == 'value'

    data = lib.LineData('key value # comment', 'devices', True)
    assert data.option == 'key'
    assert data.value == 'value'

    data = lib.LineData('    key value "extra string"', 'multipaths', True)
    assert data.option == 'key'
    assert data.value == 'value'

    data = lib.LineData(' \t\tkey "string value" junk', 'blacklist', False)
    assert data.option == 'key'
    assert data.value == 'string value'

    try:
        data = lib.LineData('key  # comment', 'devices', True)
        assert False
    except ValueError:
        pass


def test_enabled():
    values = (('yes', True), ('1', True), ('no', False), ('0', False))
    for value, is_enabled in values:
        data = lib.LineData('key ' + value, 'defaults', False)
        assert data.type == data.TYPE_OPTION
        assert data.option == 'key'
        assert data.value == value
        assert data.is_enabled() == is_enabled

    data = lib.LineData('key neither_yes_nor_no', 'defaults', False)
    assert data.type == data.TYPE_OPTION
    assert data.option == 'key'
    assert data.value == 'neither_yes_nor_no'
    assert data.is_enabled() is None


# repos/system_upgrade/el7toel8/libraries/tests/test_pam.py

import textwrap

from leapp.libraries.common.pam import PAM


def get_config(config):
    return textwrap.dedent(config).strip()


def test_PAM_parse():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = PAM('')
    modules = obj.parse(pam)

    assert len(modules) == 3
    assert 'pam_unix' in modules
    assert 'pam_sss' in modules
    assert 'pam_deny' in modules


def test_PAM_has__true():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = PAM(pam)
    assert obj.has('pam_unix')
    assert obj.has('pam_sss')
    assert obj.has('pam_deny')


def test_PAM_has__false():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = PAM(pam)
    assert not obj.has('pam_winbind')


def test_PAM_has_unknown_module__empty():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = PAM(pam)
    assert obj.has_unknown_module([])


def test_PAM_has_unknown_module__false():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    ''')

    obj = PAM(pam)
    assert not obj.has_unknown_module(['pam_unix', 'pam_sss', 'pam_deny'])


def test_PAM_has_unknown_module__true():
    pam = get_config('''
    auth sufficient pam_unix.so
    auth sufficient pam_sss.so
    auth required pam_deny.so
    session pam_ecryptfs.so
    ''')

    obj = PAM(pam)
    assert obj.has_unknown_module(['pam_unix', 'pam_sss', 'pam_deny'])


def test_PAM_read_file__non_existent():
    content = PAM.read_file('/this/does/not/exist')
    assert content == ''


def test_PAM_read_file__ok():
    content = PAM.read_file(__file__)
    assert content != ''
    assert 'test_PAM_read_file__ok' in content


# repos/system_upgrade/el7toel8/libraries/tests/test_spamassassinutils.py

import leapp.libraries.common.spamassassinutils as lib


def test_parse_sysconfig_spamassassin_begins_with_assignment():
    content = 'SPAMDOPTIONS="foo"\n# bar\n'
    pre, assignment, post = lib.parse_sysconfig_spamassassin(content)
    assert pre == ''
    assert assignment == 'SPAMDOPTIONS="foo"'
    assert post == '# bar\n'


def test_parse_sysconfig_spamassassin_ends_with_assignment():
    content = '# bar\nSPAMDOPTIONS="foo"\n'
    pre, assignment, post = lib.parse_sysconfig_spamassassin(content)
    assert pre == '# bar'
    assert assignment == 'SPAMDOPTIONS="foo"'
    assert post == ''


def test_parse_sysconfig_spamassassin_only_assignment():
    content = 'SPAMDOPTIONS="foo"\n'
    pre, assignment, post = lib.parse_sysconfig_spamassassin(content)
    assert pre == ''
    assert assignment == 'SPAMDOPTIONS="foo"'
    assert post == ''


def test_parse_sysconfig_spamassassin_no_assignment():
    content = '# foo\n'
    pre, assignment, post = lib.parse_sysconfig_spamassassin(content)
    assert pre == '# foo\n'
    assert assignment == ''
    assert post == ''


def test_parse_sysconfig_spamassassin_empty():
    content = ''
    pre, assignment, post = lib.parse_sysconfig_spamassassin(content)
    assert pre == ''
    assert assignment == ''
    assert post == ''


# repos/system_upgrade/el7toel8/libraries/tests/test_tcpwrappersutils.py

import leapp.libraries.common.tcpwrappersutils as lib
from leapp.models import DaemonList, TcpWrappersFacts


def test_config_applies_to_daemon_simple():
    daemon_list = DaemonList(value=['vsftpd'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])

    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True
    assert lib.config_applies_to_daemon(facts, 'VsfTpd') is True
    assert lib.config_applies_to_daemon(facts, 'ftp') is False
    assert lib.config_applies_to_daemon(facts, 'foo') is False


def test_config_applies_to_daemon_multiple_lists():
    list1 = DaemonList(value=['vsftpd', 'sendmail'])
    list2 = DaemonList(value=['postfix'])
    facts = TcpWrappersFacts(daemon_lists=[list1, list2])

    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True
    assert lib.config_applies_to_daemon(facts, 'sendmail') is True
    assert lib.config_applies_to_daemon(facts, 'postfix') is True
    assert lib.config_applies_to_daemon(facts, 'foo') is False


def test_config_applies_to_daemon_except():
    list1 = DaemonList(value=['all', 'except', 'sendmail'])
    list2 = DaemonList(value=['postfix'])
    facts = TcpWrappersFacts(daemon_lists=[list1, list2])

    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True
    assert lib.config_applies_to_daemon(facts, 'sendmail') is False
    assert lib.config_applies_to_daemon(facts, 'postfix') is True
    assert lib.config_applies_to_daemon(facts, 'foo') is True

    list1 = DaemonList(value=['all', 'except', 'b*', 'EXCEPT', 'bar'])
    facts = TcpWrappersFacts(daemon_lists=[list1])
    assert lib.config_applies_to_daemon(facts, 'foo') is True
    assert lib.config_applies_to_daemon(facts, 'bar') is True
    assert lib.config_applies_to_daemon(facts, 'baar') is False

    list1 = DaemonList(value=['all', 'except', 'vsftpd'])
    facts = TcpWrappersFacts(daemon_lists=[list1])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False

    list1 = DaemonList(value=['all', 'except', 'all', 'except', 'vsftpd'])
    facts = TcpWrappersFacts(daemon_lists=[list1])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    list1 = DaemonList(value=['all', 'except', 'all', 'except', 'all', 'except', 'vsftpd'])
    facts = TcpWrappersFacts(daemon_lists=[list1])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False


def test_config_applies_to_daemon_except_empty():
    daemon_list = DaemonList(value=['all', 'except'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True


def test_config_applies_to_daemon_with_host():
    list1 = DaemonList(value=['vsftpd@localhost', 'sendmail'])
    list2 = DaemonList(value=['postfix'])
    facts = TcpWrappersFacts(daemon_lists=[list1, list2])

    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True
    assert lib.config_applies_to_daemon(facts, 'sendmail') is True
    assert lib.config_applies_to_daemon(facts, 'postfix') is True
    assert lib.config_applies_to_daemon(facts, 'foo') is False


def test_config_applies_to_daemon_with_host_except():
    daemon_list = DaemonList(value=['vsftpd@localhost', 'except', 'vsftpd'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False

    # It works like this for simplicity.
    daemon_list = DaemonList(value=['vsftpd@localhost', 'except', 'vsftpd@localhost'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['vsftpd@localhost'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['all', 'except', 'vsftpd@localhost'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['all', 'except', 'all', 'except', 'vsftpd@localhost'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['all', 'except', 'all', 'except', 'all',
                                    'except', 'vsftpd@localhost'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['all', 'except', 'all', 'except', 'all', 'except', 'all',
                                    'except', 'vsftpd@localhost'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True


def test_config_applies_to_daemon_empty():
    daemon_list = DaemonList(value=[''])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False

    daemon_list = DaemonList(value=[])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False


def test_config_applies_to_daemon_whole_word():
    daemon_list = DaemonList(value=['ftp'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False


def test_config_applies_to_daemon_asterisk_wildcard():
    daemon_list = DaemonList(value=['*ftp*'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['************'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['*'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['*foo*'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False


def test_config_applies_to_daemon_question_mark_wildcard():
    daemon_list = DaemonList(value=['vs?tpd'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['vsf?tpd'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False

    daemon_list = DaemonList(value=['?'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False

    daemon_list = DaemonList(value=['??????'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True


def test_config_applies_to_daemon_all_wildcard():
    daemon_list = DaemonList(value=['all'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['aLl'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is True

    daemon_list = DaemonList(value=['al'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False

    daemon_list = DaemonList(value=['ll'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False

    daemon_list = DaemonList(value=['valld'])
    facts = TcpWrappersFacts(daemon_lists=[daemon_list])
    assert lib.config_applies_to_daemon(facts, 'vsftpd') is False


# repos/system_upgrade/el7toel8/libraries/tests/test_vsftpdutils.py

import errno

from leapp.libraries.common.testutils import make_IOError
from leapp.libraries.common.vsftpdutils import get_config_contents, get_default_config_hash


class MockFile(object):
    def __init__(self, path, content=None, to_raise=None):
        self.path = path
        self.content = content
        self.to_raise = to_raise
        self.error = False

    def read_file(self, path):
        if path != self.path:
            self.error = True
            raise ValueError
        if not self.to_raise:
            return self.content
        raise self.to_raise


def test_getting_nonexistent_config_gives_None():
    path = 'my_file'
    f = MockFile(path, to_raise=make_IOError(errno.ENOENT))

    res = get_config_contents(path, read_func=f.read_file)

    assert not f.error
    assert res is None


def test_getting_inaccessible_config_gives_None():
    path = 'my_file'
    f = MockFile(path, to_raise=make_IOError(errno.EACCES))

    res = get_config_contents(path, read_func=f.read_file)

    assert not f.error
    assert res is None


def test_getting_empty_config_gives_empty_string():
    path = 'my_file'
    f = MockFile(path, content='')

    res = get_config_contents(path, read_func=f.read_file)

    assert not f.error
    assert res == ''


def test_getting_nonempty_config_gives_the_content():
    path = 'my_file'
    content = 'foo\nbar\n'
    f = MockFile(path, content=content)

    res = get_config_contents(path, read_func=f.read_file)

    assert not f.error
    assert res == content


def test_hash_of_default_config_is_correct():
    path = '/etc/vsftpd/vsftpd.conf'
    content = 'foo\n'
    f = MockFile(path, content=content)

    h = get_default_config_hash(read_func=f.read_file)

    assert h == 'f1d2d2f924e986ac86fdf7b36c94bcdf32beec15'
    assert not f.error


def test_hash_of_nonexistent_default_config_is_None():
    path = '/etc/vsftpd/vsftpd.conf'
    f = MockFile(path, to_raise=make_IOError(errno.ENOENT))

    h = get_default_config_hash(read_func=f.read_file)

    assert h is None
    assert not f.error


# repos/system_upgrade/el7toel8/libraries/tests/test_isccfg.py
#!/usr/bin/env python
#
# Tests for bind configuration parsing

from leapp.libraries.common import isccfg

#
# Sample configuration stubs
#
named_conf_default = isccfg.MockConfig("""
//
// named.conf
//
// Provided by Red Hat bind package to configure the ISC BIND named(8) DNS
// server as a caching only nameserver (as a localhost DNS resolver only).
//
// See /usr/share/doc/bind*/sample/ for example named configuration files.
//

options {
    listen-on port 53 { 127.0.0.1; };
    listen-on-v6 port 53 { ::1; };
    directory      "/var/named";
    dump-file      "/var/named/data/cache_dump.db";
    statistics-file "/var/named/data/named_stats.txt";
    memstatistics-file "/var/named/data/named_mem_stats.txt";
    secroots-file   "/var/named/data/named.secroots";
    recursing-file  "/var/named/data/named.recursing";
    allow-query     { localhost; };

    /*
     - If you are building an AUTHORITATIVE DNS server, do NOT enable recursion.
     - If you are building a RECURSIVE (caching) DNS server, you need to enable
       recursion.
     - If your recursive DNS server has a public IP address, you MUST enable access
       control to limit queries to your legitimate users. Failing to do so will
       cause your server to become part of large scale DNS amplification
       attacks. Implementing BCP38 within your network would greatly
       reduce such attack surface
    */
    recursion yes;

    dnssec-enable yes;
    dnssec-validation yes;

    managed-keys-directory "/var/named/dynamic";

    pid-file "/run/named/named.pid";
    session-keyfile "/run/named/session.key";
};

logging {
        channel default_debug {
                file "data/named.run";
                severity dynamic;
        };
};

zone "." IN {
    type hint;
    file "named.ca";
};

# Avoid including files from bind package, may be not installed
# include "/etc/named.rfc1912.zones";
# include "/etc/named.root.key";
include "/dev/null";
""")


options_lookaside_no = isccfg.MockConfig("""
options {
    dnssec-lookaside no;
};
""")


options_lookaside_auto = isccfg.MockConfig("""
options {
    dnssec-lookaside /* no */ auto;
};
""")


options_lookaside_manual = isccfg.MockConfig("""
options {
    # make sure parser handles comments
    dnssec-lookaside "." /* comment to confuse parser */trust-anchor "dlv.isc.org";
};
""")


options_lookaside_commented = isccfg.MockConfig("""
options {
    /* dnssec-lookaside auto; */
};
""")


views_lookaside = isccfg.MockConfig("""
view "v1" IN {
    // This is auto
    dnssec-lookaside auto;
};

options {
    /* This is multi
     * line
     * comment */
    dnssec-lookaside no;
};

view "v2" {
    # Note no IN
    dnssec-lookaside "." trust-anchor "dlv.isc.org";
};
""")


def check_in_section(parser, section, key, value):
    """ Helper to check some section was found
        in configuration section and has expected value

        :type parser: IscConfigParser
        :type section: bind.ConfigSection
        :type key: str
        :param value: expected value """
    assert isinstance(section, isccfg.ConfigSection)
    cfgval = parser.find_val_section(section, key)
    assert isinstance(cfgval, isccfg.ConfigSection)
    assert cfgval.value() == value
    return cfgval


def cb_state(statement, state):
    """Callback used in IscConfigParser.walk()"""
    key = statement.var(0).value()
    state[key] = statement


def find_options(parser):
    """Replace IscConfigParser.find_option with walk use"""
    state = {}
    callbacks = {
        'options': cb_state,
    }
    assert len(parser.FILES_TO_CHECK) >= 1
    cfg = parser.FILES_TO_CHECK[0]
    parser.walk(cfg.root_section(), callbacks, state)
    options = state['options']
    if options:
        assert isinstance(options, isccfg.ConfigVariableSection)
        return options.firstblock()
    return None


# End of helpers
#
# Begin of tests


def test_lookaside_no():
    parser = isccfg.IscConfigParser(options_lookaside_no)
    assert len(parser.FILES_TO_CHECK) == 1
    opt = find_options(parser)
    check_in_section(parser, opt, "dnssec-lookaside", "no")


def test_lookaside_commented():
    parser = isccfg.IscConfigParser(options_lookaside_commented)
    assert len(parser.FILES_TO_CHECK) == 1
    opt = find_options(parser)
    assert isinstance(opt, isccfg.ConfigSection)
    lookaside = parser.find_val_section(opt, "dnssec-lookaside")
    assert lookaside is None


def test_default():
    parser = isccfg.IscConfigParser(named_conf_default)
    assert len(parser.FILES_TO_CHECK) >= 2
    opt = find_options(parser)
    check_in_section(parser, opt, "directory", '"/var/named"')
    check_in_section(parser, opt, "session-keyfile", '"/run/named/session.key"')
    check_in_section(parser, opt, "allow-query", '{ localhost; }')
    check_in_section(parser, opt, "recursion", 'yes')
    check_in_section(parser, opt, "dnssec-validation", 'yes')
    check_in_section(parser, opt, "dnssec-enable", 'yes')


def test_key_lookaside():
    parser = isccfg.IscConfigParser(options_lookaside_manual)
    opt = find_options(parser)
    key = parser.find_next_key(opt.config, opt.start+1, opt.end)
    assert isinstance(key, isccfg.ConfigSection)
    assert key.value() == 'dnssec-lookaside'
    value = parser.find_next_val(opt.config, None, key.end+1, opt.end)
    assert value.value() == '"."'
    key2 = parser.find_next_key(opt.config, value.end+1, opt.end)
    assert key2.value() == 'trust-anchor'
    value2a = parser.find_next_val(opt.config, None, key2.end+1, opt.end)
    value2b = parser.find_val(opt.config, 'trust-anchor', value.end+1, opt.end)
    assert value2b.value() == '"dlv.isc.org"'
    assert value2a.value() == value2b.value()
    value3 = parser.find_next_key(opt.config, value2b.end+1, opt.end, end_report=True)
    assert value3.value() == ';'


def test_key_lookaside_all():
    """ Test getting variable arguments after keyword """
    parser = isccfg.IscConfigParser(options_lookaside_manual)
    assert len(parser.FILES_TO_CHECK) == 1
    opt = find_options(parser)
    assert isinstance(opt, isccfg.ConfigSection)
    values = parser.find_values(opt, "dnssec-lookaside")
    assert values is not None
    assert len(values) >= 4
    key = values[0].value()
    assert key == 'dnssec-lookaside'
    assert values[1].value() == '"."'
    assert values[2].value() == 'trust-anchor'
    assert values[3].value() == '"dlv.isc.org"'
    assert values[4].value() == ';'


def test_key_lookaside_simple():
    """ Test getting variable arguments after keyword """
    parser = isccfg.IscConfigParser(options_lookaside_manual)
    assert len(parser.FILES_TO_CHECK) == 1
    stmts = parser.find('options.dnssec-lookaside')
    assert stmts is not None
    assert len(stmts) == 1
    assert isinstance(stmts[0], isccfg.ConfigVariableSection)
    values = stmts[0].values
    assert len(values) >= 4
    key = values[0].value()
    assert key == 'dnssec-lookaside'
    assert values[1].value() == '"."'
    assert values[2].value() == 'trust-anchor'
    assert values[3].value() == '"dlv.isc.org"'
    assert values[4].value() == ';'


def test_find_index():
    """ Test simplified searching for values in sections """
    parser = isccfg.IscConfigParser(named_conf_default)
    assert len(parser.FILES_TO_CHECK) >= 1
    stmts = parser.find('logging.channel.severity')
    assert stmts is not None and len(stmts) == 1
    assert isinstance(stmts[0], isccfg.ConfigVariableSection)
    values = stmts[0].values
    assert len(values) >= 1
    key = values[0].value()
    assert key == 'severity'
    assert values[1].value() == 'dynamic'
    recursion = parser.find('options.recursion')
    assert len(recursion) == 1 and len(recursion[0].values) >= 2
    assert recursion[0].values[0].value() == 'recursion'
    assert recursion[0].values[1].value() == 'yes'


def cb_view(statement, state):
    if 'view' not in state:
        state['view'] = {}
    name = statement.var(1).invalue()
    second = statement.var(2)
    if second.type() != isccfg.ConfigSection.TYPE_BLOCK:
        name = second.value() + '_' + name
    state['view'][name] = statement


def test_key_views_lookaside():
    """ Test getting variable arguments for views """

    parser = isccfg.IscConfigParser(views_lookaside)
    assert len(parser.FILES_TO_CHECK) == 1
    opt = find_options(parser)
    assert isinstance(opt, isccfg.ConfigSection)
    opt_val = parser.find_values(opt, "dnssec-lookaside")
    assert isinstance(opt_val[1], isccfg.ConfigSection)
    assert opt_val[1].value() == 'no'

    state = {}
    callbacks = {
        'view': cb_view,
    }
    assert len(parser.FILES_TO_CHECK) >= 1
    cfg = parser.FILES_TO_CHECK[0]
    parser.walk(cfg.root_section(), callbacks, state)

    views = state['view']
    assert len(views) == 2

    v1 = views['IN_v1']
    assert isinstance(v1, isccfg.ConfigVariableSection)
    v1b = v1.firstblock()
    assert isinstance(v1b, isccfg.ConfigSection)
    v1_la = parser.find_val_section(v1b, "dnssec-lookaside")
    assert isinstance(v1_la, isccfg.ConfigSection)
    assert v1_la.value() == 'auto'

    v2 = views['v2']
    assert isinstance(v2, isccfg.ConfigVariableSection)
    v2b = v2.firstblock()
    assert isinstance(v2b, isccfg.ConfigSection)
    v2_la = parser.find_values(v2b, "dnssec-lookaside")
    assert isinstance(v2_la[1], isccfg.ConfigSection)
    assert v2_la[1].value() == '"."'
    assert isinstance(v2_la[3], isccfg.ConfigSection)
    assert v2_la[3].value() == '"dlv.isc.org"'


def test_remove_comments():
    """ Test removing comments works as expected """

    parser = isccfg.IscConfigParser(views_lookaside)
    assert len(parser.FILES_TO_CHECK) == 1
    cfg = parser.FILES_TO_CHECK[0]
    assert isinstance(cfg, isccfg.ConfigFile)
    removed_comments = parser._remove_comments(cfg.buffer)
    assert len(removed_comments) < len(cfg.buffer)
    replaced_comments = parser._replace_comments(cfg.buffer)
    assert len(replaced_comments) == len(cfg.buffer)
    assert 'This is auto' not in replaced_comments
    assert 'comment' not in replaced_comments
    assert 'Note no IN' not in replaced_comments


def test_walk():
    """ Test walk function of parser """

    callbacks = {
        'options': cb_state,
        'dnssec-lookaside': cb_state,
        'dnssec-validation': cb_state,
    }
    state = {}
    parser = isccfg.IscConfigParser(views_lookaside)
    assert len(parser.FILES_TO_CHECK) == 1
    cfg = parser.FILES_TO_CHECK[0]
    parser.walk(cfg.root_section(), callbacks, state)
    assert 'options' in state
    assert 'dnssec-lookaside' in state
    assert 'dnssec-validation' not in state


if __name__ == '__main__':
    test_key_views_lookaside()
