etails': 'An exception raised during containers listing {}'.format(str(cpe))}
        )
    for line in output['stdout'].splitlines():
        container_name = line.split()[-1]
        if re.match(CONTAINER, container_name):
            return container_name
    return container_name


def get_ceph_lvm_list():
    base_cmd = ['ceph-volume', 'lvm', 'list', '--format', 'json']
    container_binary = 'podman' if has_package(InstalledRPM, 'podman') else \
        'docker' if has_package(InstalledRPM, 'docker') else ''
    if container_binary == '':
        cmd_ceph_lvm_list = base_cmd
    else:
        container_name = select_osd_container(container_binary)
        cmd_ceph_lvm_list = [container_binary, 'exec', container_name]
        cmd_ceph_lvm_list.extend(base_cmd)
    try:
        output = run(cmd_ceph_lvm_list)
    except CalledProcessError as cpe:
        raise StopActorExecutionError(
            'Could not retrieve the ceph volumes list',
            details={'details': 'An exception raised while retrieving ceph volumes {}'.format(str(cpe))}
        )
    try:
        json_output = json.loads(output['stdout'])
    except ValueError as jve:
        raise StopActorExecutionError(
            'Could not load json file containing ceph volume list',
            details={'details': 'json file wrong format {}'.format(str(jve))}
        )
    return json_output


def encrypted_osds_list():
    result = []
    if os.path.isfile(CEPH_CONF):
        output = get_ceph_lvm_list()
        result = [output[key][0]['lv_uuid'] for key in output if output[key][0]['tags']['ceph.encrypted']]
    return result


# repos/system_upgrade/common/actors/cephvolumescan/tests/test_cephvolumescan.py

import pytest
from mock import Mock, patch

from leapp.libraries.actor import cephvolumescan
from leapp.models import InstalledRPM, LsblkEntry, RPM, StorageInfo
from leapp.reporting import Report

CONT_PS_COMMAND_OUTPUT = {
    "stdout":
    """CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
    50d96fe72019 registry.redhat.io/rhceph/rhceph-4-rhel8:latest "/opt/ceph-contain..." \
        2 weeks ago Up 2 weeks ceph-osd-0
    f93c17b49c40 registry.redhat.io/rhceph/rhceph-4-rhel8:latest "/opt/ceph-contain..." \
        2 weeks ago Up 2 weeks ceph-osd-1
    0669880c52dc registry.redhat.io/rhceph/rhceph-4-rhel8:latest "/opt/ceph-contain..." \
        2 weeks ago Up 2 weeks ceph-mgr-ceph4-standalone
    d7068301294e registry.redhat.io/rhceph/rhceph-4-rhel8:latest "/opt/ceph-contain..." \
        2 weeks ago Up 2 weeks ceph-mon-ceph4-standalone
    63de6d00f241 registry.redhat.io/openshift4/ose-prometheus-alertmanager:4.1 "/bin/alertmanager..." \
        2 weeks ago Up 2 weeks alertmanager
    28ed65960c80 registry.redhat.io/rhceph/rhceph-4-dashboard-rhel8:4 "/run.sh" \
        2 weeks ago Up 2 weeks grafana-server
    f4b300d7a11f registry.redhat.io/openshift4/ose-prometheus-node-exporter:v4.1 "/bin/node_exporte..." \
        2 weeks ago Up 2 weeks node-exporter
    95a03700b3ff registry.redhat.io/openshift4/ose-prometheus:4.1 "/bin/prometheus -..." \
        2 weeks ago Up 2 weeks prometheus"""
    }

CEPH_VOLUME_OUTPUT = {
    "stdout": """{
        "0":[
            {
                "devices":[
                    "/dev/sda"
                ],
                "lv_name":"osd-block-c5215ba7-517b-45c7-88df-37a03eeaa0e9",
                "lv_uuid":"Tyc0TH-RDxr-ebAF-9mWF-Kh5R-YnvJ-cEcGVn",
                "tags":{
                    "ceph.encrypted":"1"
                },
                "type":"block",
                "vg_name":"ceph-a696c40d-6b1d-448d-a40e-fadca22b64bc"
            }
        ]
    }"""
}

CEPH_LVM_LIST = {
    '0': [{'devices': ['/dev/sda'],
           'lv_name': 'osd-block-c5215ba7-517b-45c7-88df-37a03eeaa0e9',
           'lv_uuid': 'Tyc0TH-RDxr-ebAF-9mWF-Kh5R-YnvJ-cEcGVn',
           'tags': {'ceph.encrypted': '1'},
           'type': 'block',
           'vg_name': 'ceph-a696c40d-6b1d-448d-a40e-fadca22b64bc'}]
    }


@patch('leapp.libraries.actor.cephvolumescan.run')
def test_select_osd_container(m_run):

    m_run.return_value = CONT_PS_COMMAND_OUTPUT

    assert cephvolumescan.select_osd_container('docker') == "ceph-osd-0"


@patch('leapp.libraries.actor.cephvolumescan.has_package')
@patch('leapp.libraries.actor.cephvolumescan.select_osd_container')
@patch('leapp.libraries.actor.cephvolumescan.run')
def test_get_ceph_lvm_list(m_run, m_osd_container, m_has_package):

    m_has_package.return_value = True
    m_osd_container.return_value = 'podman'
    m_run.return_value = CEPH_VOLUME_OUTPUT

    assert cephvolumescan.get_ceph_lvm_list() == CEPH_LVM_LIST


@patch('leapp.libraries.actor.cephvolumescan.os.path.isfile')
@patch('leapp.libraries.actor.cephvolumescan.get_ceph_lvm_list')
def test_encrypted_osds_list(m_get_ceph_lvm_list, m_isfile):

    m_get_ceph_lvm_list.return_value = CEPH_LVM_LIST
    m_isfile.return_value = True

    assert cephvolumescan.encrypted_osds_list() == ['Tyc0TH-RDxr-ebAF-9mWF-Kh5R-YnvJ-cEcGVn']


# repos/system_upgrade/common/actors/checkbootavailspace/libraries/checkbootavailspace.py

from __future__ import division

from os import statvfs

from leapp import reporting

MIN_AVAIL_BYTES_FOR_BOOT = 100 * 2**20  # 100 MiB


def check_avail_space_on_boot(boot_avail_space_getter):
    avail_bytes = boot_avail_space_getter()
    if is_additional_space_required(avail_bytes):
        inhibit_upgrade(avail_bytes)


def get_avail_bytes_on_boot():
    boot_stat = statvfs('/boot')
    return boot_stat.f_frsize * boot_stat.f_bavail


def is_additional_space_required(avail_bytes):
    return avail_bytes < MIN_AVAIL_BYTES_FOR_BOOT


def inhibit_upgrade(avail_bytes):
    additional_mib_needed = (MIN_AVAIL_BYTES_FOR_BOOT - avail_bytes) / 2**20
    # we use "reporting.report_generic" to allow mocking in the tests
    # WIP ^^ check if this still applies
    reporting.create_report([
        reporting.Title('Not enough space on /boot'),
        reporting.Summary(
            '/boot needs additional {0} MiB to be able to accommodate the upgrade initramfs and new kernel.'.format(
             additional_mib_needed)
        ),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Groups([reporting.Groups.FILESYSTEM]),
        reporting.Groups([reporting.Groups.INHIBITOR]),
        reporting.RelatedResource('directory', '/boot')
    ])


# repos/system_upgrade/common/actors/checkbootavailspace/tests/unit_test_checkbootavailspace.py

from __future__ import division

from leapp import reporting
from leapp.libraries.actor.checkbootavailspace import (
    check_avail_space_on_boot,
    inhibit_upgrade,
    MIN_AVAIL_BYTES_FOR_BOOT
)
from leapp.libraries.common.testutils import create_report_mocked
from leapp.utils.report import is_inhibitor


class fake_get_avail_bytes_on_boot(object):
    def __init__(self, size):
        self.size = size

    def __call__(self, *args):
        return self.size


def test_not_enough_space_available(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    # Test 0 bytes available /boot
    get_avail_bytes_on_boot = fake_get_avail_bytes_on_boot(0)
    check_avail_space_on_boot(get_avail_bytes_on_boot)

    # Test 0.1 MiB less then required in /boot
    get_avail_bytes_on_boot = fake_get_avail_bytes_on_boot(MIN_AVAIL_BYTES_FOR_BOOT - 0.1 * 2**20)
    check_avail_space_on_boot(get_avail_bytes_on_boot)

    assert reporting.create_report.called == 2


def test_enough_space_available(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    get_avail_bytes_on_boot = fake_get_avail_bytes_on_boot(MIN_AVAIL_BYTES_FOR_BOOT)
    check_avail_space_on_boot(get_avail_bytes_on_boot)

    assert reporting.create_report.called == 0


def test_inhibit_upgrade(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    # Test 4.2 MiB available on /boot
    bytes_available = 4.2 * 2**20
    inhibit_upgrade(bytes_available)

    assert reporting.create_report.called == 1
    assert is_inhibitor(reporting.create_report.report_fields)
    mib_needed = (MIN_AVAIL_BYTES_FOR_BOOT - bytes_available) / 2**20
    assert "needs additional {0} MiB".format(mib_needed) in reporting.create_report.report_fields['summary']


# repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/libraries/checkdddd.py

from collections import defaultdict
from enum import IntEnum

from leapp import reporting
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 DetectedDeviceOrDriver


class MessagingClass(IntEnum):
    UNKNOWN = 0
    DRIVERS = 1
    DEVICES = 2
    CPUS = 3


def create_inhibitors(inhibiting_entries):
    if not inhibiting_entries:
        return

    drivers = inhibiting_entries.get(MessagingClass.DRIVERS)
    if drivers:
        reporting.create_report([
            reporting.Title(
                'Leapp detected loaded kernel drivers which have been removed '
                'in RHEL {}. Upgrade cannot proceed.'.format(get_target_major_version())
            ),
            reporting.Summary(
                (
                    'Support for the following RHEL {source} device drivers has been removed in RHEL {target}:\n'
                    '     - {drivers}\n'
                ).format(
                    drivers='\n     - '.join([entry.driver_name for entry in drivers]),
                    target=get_target_major_version(),
                    source=get_source_major_version(),
                )
            ),
            reporting.Audience('sysadmin'),
            reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.DRIVERS]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.INHIBITOR])
        ])

    devices = inhibiting_entries.get(MessagingClass.DEVICES)
    if devices:
        reporting.create_report([
            reporting.Title(
                'Leapp detected devices which are no longer supported in RHEL {}. Upgrade cannot proceed.'.format(
                    get_target_major_version())
            ),
            reporting.Summary(
                (
                    'Support for the following devices has been removed in RHEL {target}:\n'
                    '     - {devices}\n'
                ).format(
                    devices='\n     - '.join(['{name} ({pci})'.format(name=entry.device_name,
                                             pci=entry.device_id) for entry in devices]),
                    target=get_target_major_version(),
                )
            ),
            reporting.Audience('sysadmin'),
            reporting.Groups([reporting.Groups.KERNEL]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.INHIBITOR])
        ])

    cpus = inhibiting_entries.get(MessagingClass.CPUS)
    if cpus:
        reporting.create_report([
            reporting.Title(
                'Leapp detected a processor which is no longer supported in RHEL {}. Upgrade cannot proceed.'.format(
                    get_target_major_version())
            ),
            reporting.Summary(
                (
                    'Support for the following processors has been removed in RHEL {target}:\n'
                    '     - {processors}\n'
                ).format(
                    processors='\n     - '.join([entry.device_name for entry in cpus]),
                    target=get_target_major_version(),
                )
            ),
            reporting.Audience('sysadmin'),
            reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.BOOT]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.INHIBITOR])
        ])


def create_warnings(unmaintained_entries):
    if not unmaintained_entries:
        return

    drivers = unmaintained_entries.get(MessagingClass.DRIVERS)
    if drivers:
        reporting.create_report([
            reporting.Title(
                'Leapp detected loaded kernel drivers which are no longer maintained in RHEL {}.'.format(
                    get_target_major_version())
            ),
            reporting.Summary(
                (
                    'The following RHEL {source} device drivers are no longer maintained RHEL {target}:\n'
                    '     - {drivers}\n'
                ).format(
                    drivers='\n     - '.join([entry.driver_name for entry in drivers]),
                    target=get_target_major_version(),
                    source=get_source_major_version(),
                )
            ),
            reporting.Audience('sysadmin'),
            reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.DRIVERS]),
            reporting.Severity(reporting.Severity.HIGH),
        ])

    devices = unmaintained_entries.get(MessagingClass.DEVICES)
    if devices:
        reporting.create_report([
            reporting.Title(
                'Leapp detected devices which are no longer maintained in RHEL {}'.format(
                    get_target_major_version())
            ),
            reporting.Summary(
                (
                    'The support for the following devices has been removed in RHEL {target} and '
                    'are no longer maintained:\n     - {devices}\n'
                ).format(
                    devices='\n     - '.join(['{name} ({pci})'.format(name=entry.device_name,
                                             pci=entry.device_id) for entry in devices]),
                    target=get_target_major_version(),
                )
            ),
            reporting.Audience('sysadmin'),
            reporting.Groups([reporting.Groups.KERNEL]),
            reporting.Severity(reporting.Severity.HIGH),
        ])

    cpus = unmaintained_entries.get(MessagingClass.CPUS)
    if cpus:
        reporting.create_report([
            reporting.Title(
                'Leapp detected a processor which is no longer maintained in RHEL {}.'.format(
                    get_target_major_version())
            ),
            reporting.Summary(
                (
                    'The following processors are no longer maintained in RHEL {target}:\n'
                    '     - {processors}\n'
                ).format(
                    processors='\n     - '.join([entry.device_name for entry in cpus]),
                    target=get_target_major_version(),
                )
            ),
            reporting.Audience('sysadmin'),
            reporting.Groups([reporting.Groups.KERNEL, reporting.Groups.BOOT]),
            reporting.Severity(reporting.Severity.HIGH),
        ])


def classify(entry):
    if entry.device_type == 'pci':
        if entry.device_id:
            return MessagingClass.DEVICES
        return MessagingClass.DRIVERS
    if entry.device_type == 'cpu':
        return MessagingClass.CPUS
    return MessagingClass.UNKNOWN


def process():
    target_version = int(get_target_major_version())
    inhibiting = defaultdict(list)
    unmaintained = defaultdict(list)
    for entry in api.consume(DetectedDeviceOrDriver):
        if target_version not in entry.available_in_rhel:
            inhibiting[classify(entry)].append(entry)
        elif target_version not in entry.maintained_in_rhel:
            unmaintained[classify(entry)].append(entry)
    create_inhibitors(inhibiting)
    create_warnings(unmaintained)


# repos/system_upgrade/common/actors/checketcreleasever/libraries/checketcreleasever.py

from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.models import PkgManagerInfo, RHUIInfo


def handle_etc_releasever():

    target_version = api.current_actor().configuration.version.target
    reporting.create_report([
        reporting.Title(
            'Release version in /etc/dnf/vars/releasever will be set to the current target release'
        ),
        reporting.Summary(
            'On this system, Leapp detected "releasever" variable is either configured through DNF/YUM configuration '
            'file and/or the system is using RHUI infrastructure. In order to avoid issues with repofile URLs '
            '(when --release option is not provided) in cases where there is the previous major.minor version value '
            'in the configuration, release version will be set to the target release version ({}). This will also '
            'ensure the system stays on the target version after the upgrade. In order to enable latest minor version '
            'updates, you can remove "/etc/dnf/vars/releasever" file.'.format(
                target_version
            )
        ),
        reporting.Severity(reporting.Severity.INFO),
        reporting.Groups([reporting.Groups.UPGRADE_PROCESS]),
    ])


def process():
    pkg_facts = next(api.consume(PkgManagerInfo), None)
    rhui_facts = next(api.consume(RHUIInfo), None)
    if pkg_facts and pkg_facts.etc_releasever is not None or rhui_facts:
        handle_etc_releasever()
    else:
        api.current_logger().debug(
            'Skipping execution. "releasever" is not set in DNF/YUM vars directory and no RHUIInfo has '
            'been produced'
        )


# repos/system_upgrade/common/actors/checketcreleasever/tests/test_checketcreleasever.py

import os

import pytest

from leapp import reporting
from leapp.libraries.actor import checketcreleasever
from leapp.libraries.common.testutils import (
    create_report_mocked,
    CurrentActorMocked,
    logger_mocked
)
from leapp.libraries.stdlib import api
from leapp.models import PkgManagerInfo, Report, RHUIInfo


@pytest.mark.parametrize('exists', [True, False])
def test_etc_releasever(monkeypatch, exists):
    pkg_mgr_msg = [PkgManagerInfo(etc_releasever='7.7')] if exists else []
    expected_rel_ver = '6.10'

    mocked_report = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=pkg_mgr_msg, dst_ver=expected_rel_ver
        )
    )
    monkeypatch.setattr(reporting, 'create_report', mocked_report)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    checketcreleasever.process()

    if exists:
        assert reporting.create_report.called == 1
        assert expected_rel_ver in mocked_report.report_fields['summary']
        assert not api.current_logger.dbgmsg
    else:
        assert not reporting.create_report.called
        assert api.current_logger.dbgmsg


def test_etc_releasever_empty(monkeypatch):
    pkg_mgr_msg = [PkgManagerInfo(etc_releasever=None)]
    expected_rel_ver = '6.10'

    mocked_report = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=pkg_mgr_msg, dst_ver=expected_rel_ver
        )
    )
    monkeypatch.setattr(reporting, 'create_report', mocked_report)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    checketcreleasever.process()

    assert not reporting.create_report.called
    assert api.current_logger.dbgmsg


@pytest.mark.parametrize('is_rhui', [True, False])
def test_etc_releasever_rhui(monkeypatch, is_rhui):
    rhui_msg = [RHUIInfo(provider='aws')] if is_rhui else []
    expected_rel_ver = '6.10'

    mocked_report = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=rhui_msg, dst_ver=expected_rel_ver
        )
    )
    monkeypatch.setattr(reporting, 'create_report', mocked_report)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    checketcreleasever.process()

    if is_rhui:
        assert reporting.create_report.called == 1
        assert expected_rel_ver in mocked_report.report_fields['summary']
        assert not api.current_logger.dbgmsg
    else:
        assert not reporting.create_report.called
        assert api.current_logger.dbgmsg


def test_etc_releasever_neither(monkeypatch):
    mocked_report = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(reporting, 'create_report', mocked_report)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    checketcreleasever.process()

    assert not reporting.create_report.called
    assert api.current_logger.dbgmsg


def test_etc_releasever_both(monkeypatch):
    msgs = [RHUIInfo(provider='aws'), PkgManagerInfo(etc_releasever='7.7')]
    expected_rel_ver = '6.10'

    mocked_report = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=msgs, dst_ver=expected_rel_ver
        )
    )
    monkeypatch.setattr(reporting, 'create_report', mocked_report)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    checketcreleasever.process()

    assert reporting.create_report.called == 1
    assert expected_rel_ver in mocked_report.report_fields['summary']
    assert not api.current_logger.dbgmsg


# repos/system_upgrade/common/actors/checkfips/tests/unit_test_checkfips.py

import pytest

from leapp.models import KernelCmdline, KernelCmdlineArg, Report
from leapp.snactor.fixture import current_actor_context

ballast1 = [KernelCmdlineArg(key=k, value=v) for k, v in [
    ('BOOT_IMAGE', '/vmlinuz-3.10.0-1127.el7.x86_64'),
    ('root', '/dev/mapper/rhel_ibm--p8--kvm--03--guest--02-root'),
    ('ro', ''),
    ('console', 'tty0'),
    ('console', 'ttyS0,115200'),
    ('rd_NO_PLYMOUTH', '')]]
ballast2 = [KernelCmdlineArg(key=k, value=v) for k, v in [
    ('crashkernel', 'auto'),
    ('rd.lvm.lv', 'rhel_ibm-p8-kvm-03-guest-02/root'),
    ('rd.lvm.lv', 'rhel_ibm-p8-kvm-03-guest-02/swap'),
    ('rhgb', ''),
    ('quiet', ''),
    ('LANG', 'en_US.UTF-8')]]


@pytest.mark.parametrize('parameters,expected_report', [
    ([], False),
    ([KernelCmdlineArg(key='fips', value='')], False),
    ([KernelCmdlineArg(key='fips', value='0')], False),
    ([KernelCmdlineArg(key='fips', value='1')], True),
    ([KernelCmdlineArg(key='fips', value='11')], False),
    ([KernelCmdlineArg(key='fips', value='yes')], False)
])
def test_check_fips(current_actor_context, parameters, expected_report):
    cmdline = KernelCmdline(parameters=ballast1+parameters+ballast2)
    current_actor_context.feed(cmdline)
    current_actor_context.run()
    if expected_report:
        assert current_actor_context.consume(Report)
    else:
        assert not current_actor_context.consume(Report)


# repos/system_upgrade/common/actors/checkgrubcore/tests/test_checkgrubcore.py

from leapp.libraries.common.config import mock_configs
from leapp.models import FirmwareFacts, GrubDevice, UpdateGrub
from leapp.reporting import Report


def test_actor_update_grub(current_actor_context):
    current_actor_context.feed(FirmwareFacts(firmware='bios'))
    current_actor_context.feed(GrubDevice(grub_device='/dev/vda'))
    current_actor_context.run(config_model=mock_configs.CONFIG)
    assert current_actor_context.consume(Report)
    assert current_actor_context.consume(UpdateGrub)
    assert current_actor_context.consume(UpdateGrub)[0].grub_device == '/dev/vda'


def test_actor_no_grub_device(current_actor_context):
    current_actor_context.feed(FirmwareFacts(firmware='bios'))
    current_actor_context.run(config_model=mock_configs.CONFIG)
    assert current_actor_context.consume(Report)
    assert not current_actor_context.consume(UpdateGrub)


def test_actor_with_efi(current_actor_context):
    current_actor_context.feed(FirmwareFacts(firmware='efi'))
    current_actor_context.run(config_model=mock_configs.CONFIG)
    assert not current_actor_context.consume(Report)
    assert not current_actor_context.consume(UpdateGrub)


def test_s390x(current_actor_context):
    current_actor_context.feed(FirmwareFacts(firmware='bios'))
    current_actor_context.feed(GrubDevice(grub_device='/dev/vda'))
    current_actor_context.run(config_model=mock_configs.CONFIG_S390X)
    assert not current_actor_context.consume(Report)
    assert not current_actor_context.consume(UpdateGrub)


# repos/system_upgrade/common/actors/checkipaserver/libraries/checkipaserver.py

from leapp import reporting
from leapp.libraries.common.config.version import get_source_major_version

MIGRATION_GUIDE_7 = (
    "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux"
    "/8/html/installing_identity_management/migrate-7-to-8_migrating"
    )
# TBD: update the doc url when migration guide 8->9 becomes available
MIGRATION_GUIDE_8 = "https://red.ht/IdM-upgrading-RHEL-8-to-RHEL-9"
MIGRATION_GUIDES = {
    '7': MIGRATION_GUIDE_7,
    '8': MIGRATION_GUIDE_8
}


def ipa_inhibit_upgrade(ipainfo):
    """
    Create upgrade inhibitor for configured ipa-server
    """
    entries = [
        reporting.Title(
            "ipa-server does not support in-place upgrade"
        ),
        reporting.Summary(
            "An IdM server installation was detected on the system. IdM "
            "does not support in-place upgrade."
        ),
        reporting.Remediation(
            hint="Follow the IdM RHEL migration guide lines."
        ),
        reporting.ExternalLink(
            url=MIGRATION_GUIDES[get_source_major_version()],
            title="IdM migration guide",
        ),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Groups([reporting.Groups.INHIBITOR]),
        reporting.Groups([reporting.Groups.SERVICES]),
        reporting.RelatedResource("package", "ipa-server"),
    ]
    return reporting.create_report(entries)


def ipa_warn_pkg_installed(ipainfo):
    """
    Warn that unused ipa-server package is installed
    """
    if ipainfo.is_client_configured:
        summary = (
            "The ipa-server package is installed but only IdM client is "
            "configured on this system."
        )
    else:
        summary = (
            "The ipa-server package is installed but neither IdM server "
            "nor client is configured on this system."
        )
    entries = [
        reporting.Title(
            "ipa-server package is installed but no IdM is configured"
        ),
        reporting.Summary(summary),
        reporting.Remediation(
            hint="Remove unused ipa-server package",
            commands=[["yum", "remove", "-y", "ipa-server"]],
        ),
        reporting.ExternalLink(
            url=MIGRATION_GUIDES[get_source_major_version()],
            title="Migrating IdM from RHEL 7 to 8",
        ),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Groups([reporting.Groups.SERVICES]),
        reporting.RelatedResource("package", "ipa-server"),
    ]
    return reporting.create_report(entries)


# repos/system_upgrade/common/actors/checkipaserver/tests/test_check_ipa_server_checkipaserver.py

import pytest

from leapp.libraries.common.config import version
from leapp.models import IpaInfo
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context
from leapp.utils.report import is_inhibitor


def mock_ipa_info(client, server_pkg, server_configured):
    return IpaInfo(
        has_client_package=client,
        is_client_configured=client,
        has_server_package=server_pkg,
        is_server_configured=server_configured,
    )


@pytest.mark.parametrize('src_v', ['7', '8'])
def test_inhibit_ipa_configured(monkeypatch, current_actor_context, src_v):
    monkeypatch.setattr(version, "get_source_major_version", lambda: src_v)
    current_actor_context.feed(mock_ipa_info(True, True, True))
    current_actor_context.run()
    reports = current_actor_context.consume(Report)

    assert len(reports) == 1
    fields = reports[0].report
    assert is_inhibitor(fields)
    assert "ipa-server" in fields["title"]


@pytest.mark.parametrize('src_v', ['7', '8'])
def test_warn_server_pkg(monkeypatch, current_actor_context, src_v):
    monkeypatch.setattr(version, "get_source_major_version", lambda: src_v)
    current_actor_context.feed(mock_ipa_info(True, True, False))
    current_actor_context.run()
    reports = current_actor_context.consume(Report)

    assert len(reports) == 1
    fields = reports[0].report
    assert not is_inhibitor(fields)
    assert "ipa-server" in fields["title"]


def test_client_only(current_actor_context):
    current_actor_context.feed(mock_ipa_info(True, False, False))
    current_actor_context.run()
    reports = current_actor_context.consume(Report)

    assert not reports


def test_no_ipa(current_actor_context):
    current_actor_context.feed(mock_ipa_info(False, False, False))
    current_actor_context.run()
    reports = current_actor_context.consume(Report)

    assert not reports


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

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

min_req_memory = {
    architecture.ARCH_X86_64: 1572864,  # 1.5G
    architecture.ARCH_ARM64: 1572864,  # 1.5G
    architecture.ARCH_PPC64LE: 3145728,  # 3G
    architecture.ARCH_S390X: 1572864,  # 1.5G
}


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 {} are not met'.format(version.get_target_major_version())
        summary = 'Memory detected: {} MiB, required: {} MiB'.format(
            int(minimum_req_error['detected'] / 1024),  # noqa: W1619; pylint: disable=old-division
            int(minimum_req_error['minimal_req'] / 1024),  # noqa: W1619; pylint: disable=old-division
        )
        reporting.create_report([
                          reporting.Title(title),
                          reporting.Summary(summary),
                          reporting.Severity(reporting.Severity.HIGH),
                          reporting.Groups([reporting.Groups.SANITY, reporting.Groups.INHIBITOR]),
                          reporting.ExternalLink(
                            url='https://access.redhat.com/articles/rhel-limits',
                            title='Red Hat Enterprise Linux Technology Capabilities and Limits'
                          ),
                      ])


# repos/system_upgrade/common/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
from leapp.utils.report import is_inhibitor


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 is_inhibitor(reporting.create_report.report_fields)


# repos/system_upgrade/common/actors/checkmountoptions/libraries/checkmountoptions.py

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


def inhibit_upgrade_due_var_with_noexec(mountpoint, found_in_fstab=False):
    summary = (
        'Leapp detected that the {0} mountpoint is mounted with the "noexec" option, '
        'which prevents binaries necessary for the upgrade from being executed. '
        'The upgrade process cannot continue with {0} mounted using the "noexec" option.'
    )

    if found_in_fstab:
        hint = (
            'Temporarily remove the "noexec" option from {0} entry in /etc/fstab until the system is upgraded, '
            'and remount the partition without the "noexec" option.'
        )
        related_resource = [reporting.RelatedResource('file', '/etc/fstab')]
    else:
        hint = (
            'Remount {0} without the noexec option and make sure the change is persistent'
            'during the entire in-place upgrade process.'
        )
        related_resource = []

    reporting.create_report([
        reporting.Title(
            'Detected partitions mounted with the "noexec" option, preventing a successful in-place upgrade.'
        ),
        reporting.Summary(summary.format(mountpoint)),
        reporting.Remediation(hint=hint.format(mountpoint)),
        reporting.RelatedResource('file', '/etc/fstab'),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Groups([reporting.Groups.FILESYSTEM]),
        reporting.Groups([reporting.Groups.INHIBITOR]),
    ] + related_resource)


def find_mount_entry_with_mountpoint(mount_entries, mountpoint):
    for mount_entry in mount_entries:
        if mount_entry.mount == mountpoint:
            return mount_entry
    return None


def find_fstab_entry_with_mountpoint(fstab_entries, mountpoint):
    for fstab_entry in fstab_entries:
        if fstab_entry.fs_file == mountpoint:
            return fstab_entry
    return None


def check_noexec_on_var(storage_info):
    """Check for /var or /var/lib being mounted with noexec mount option."""

    # Order of checking is important as mount options on /var/lib override those on /var
    mountpoints_to_check = ('/var/lib/leapp', '/var/lib', '/var')
    for mountpoint in mountpoints_to_check:
        fstab_entry = find_fstab_entry_with_mountpoint(storage_info.fstab, mountpoint)
        if fstab_entry and 'noexec' in fstab_entry.fs_mntops.split(','):
            inhibit_upgrade_due_var_with_noexec(fstab_entry.fs_file, found_in_fstab=True)
            return  # Do not check further as present mounts would likely reflect fstab

    # Make sure present mountpoints don't contain noexec as well - user might have fixed noexec in fstab
    # but did not remount the partition, or, less likely, mounted the partition without creating a fstab entry
    for mountpoint in mountpoints_to_check:
        mount_entry = find_mount_entry_with_mountpoint(storage_info.mount, mountpoint)
        if mount_entry and 'noexec' in mount_entry.options.split(','):
            inhibit_upgrade_due_var_with_noexec(mount_entry.mount, found_in_fstab=False)
            return


def check_mount_options():
    for storage_info in api.consume(StorageInfo):
        check_noexec_on_var(storage_info)


# repos/system_upgrade/common/actors/checkmountoptions/tests/test_checkmountoptions.py

from collections import namedtuple
from functools import partial

import pytest

from leapp import reporting
from leapp.libraries.actor.checkmountoptions import check_mount_options
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import FstabEntry, MountEntry, StorageInfo


@pytest.mark.parametrize(
    ('fstab_entries', 'mounts', 'should_inhibit'),
    [
        (
            (('/var', 'default'), ),
            (('/var', 'default'), ),
            False
        ),
        (
            (('/var', 'default'), ('/var/lib', 'default'), ),
            (('/var', 'default'), ('/var/lib', 'default'), ),
            False
        ),
        (
            (('/var', 'default'), ('/var/lib/leapp', 'noexec')),
            (('/var', 'default'), ('/var/lib/leapp', 'noexec')),
            True
        ),
        (
            (('/var', 'defaults'), ('/var/lib', 'noexec')),
            (('/var', 'noexec'), ('/var/lib', 'noexec')),
            True
        ),
        (
            (('/var', 'noexec'), ('/var/lib', 'defaults')),
            (('/var', 'noexec'), ('/var/lib', 'noexec')),
            True
        ),
    ]
)
def test_var_mounted_with_noexec_is_detected(monkeypatch, fstab_entries, mounts, should_inhibit):
    mounts = [
        MountEntry(name='/dev/sdaX', tp='ext4', mount=mountpoint, options=options) for mountpoint, options in mounts
    ]

    fstab_entries = [
        FstabEntry(fs_spec='', fs_file=mountpoint, fs_vfstype='',
                   fs_mntops=opts, fs_freq='0', fs_passno='0') for mountpoint, opts in fstab_entries
    ]

    storage_info = StorageInfo(mount=mounts, fstab=fstab_entries)

    created_reports = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[storage_info]))
    monkeypatch.setattr(reporting, 'create_report', created_reports)

    check_mount_options()

    assert bool(created_reports.called) == should_inhibit


# repos/system_upgrade/common/actors/checknfs/tests/test_checknfs.py

import pytest

from leapp.models import FstabEntry, MountEntry, StorageInfo, SystemdMountEntry
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context
from leapp.utils.report import is_inhibitor


@pytest.mark.parametrize('nfs_fstype', ('nfs', 'nfs4'))
def test_actor_with_systemdmount_entry(current_actor_context, nfs_fstype):
    with_systemdmount_entry = [SystemdMountEntry(node="nfs", path="n/a", model="n/a",
                                                 wwn="n/a", fs_type=nfs_fstype, label="n/a",
                                                 uuid="n/a")]
    current_actor_context.feed(StorageInfo(systemdmount=with_systemdmount_entry))
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report
    assert is_inhibitor(report_fields)


def test_actor_without_systemdmount_entry(current_actor_context):
    without_systemdmount_entry = [SystemdMountEntry(node="/dev/sda1",
                                                    path="pci-0000:00:17.0-ata-2",
                                                    model="TOSHIBA_THNSNJ512GDNU_A",
                                                    wwn="0x500080d9108e8753",
                                                    fs_type="ext4", label="n/a",
                                                    uuid="5675d309-eff7-4eb1-9c27-58bc5880ec72")]
    current_actor_context.feed(StorageInfo(systemdmount=without_systemdmount_entry))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


@pytest.mark.parametrize('nfs_fstype', ('nfs', 'nfs4'))
def test_actor_with_fstab_entry(current_actor_context, nfs_fstype):
    with_fstab_entry = [FstabEntry(fs_spec="lithium:/mnt/data", fs_file="/mnt/data",
                                   fs_vfstype=nfs_fstype,
                                   fs_mntops="noauto,noatime,rsize=32768,wsize=32768",
                                   fs_freq="0", fs_passno="0")]
    current_actor_context.feed(StorageInfo(fstab=with_fstab_entry))
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report
    assert is_inhibitor(report_fields)


def test_actor_without_fstab_entry(current_actor_context):
    without_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=without_fstab_entry))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_actor_with_nfsd(current_actor_context):
    with_nfsd = [MountEntry(name="nfsd", mount="/proc/fs/nfsd", tp="nfsd", options="rw,relatime")]
    current_actor_context.feed(StorageInfo(mount=with_nfsd))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


@pytest.mark.parametrize('nfs_fstype', ('nfs', 'nfs4'))
def test_actor_with_mount_share(current_actor_context, nfs_fstype):
    with_mount_share = [MountEntry(name="nfs", mount="/mnt/data", tp=nfs_fstype,
                                   options="rw,nosuid,nodev,relatime,user_id=1000,group_id=1000")]
    current_actor_context.feed(StorageInfo(mount=with_mount_share))
    current_actor_context.run()
    report_fields = current_actor_context.consume(Report)[0].report
    assert is_inhibitor(report_fields)


def test_actor_without_mount_share(current_actor_context):
    without_mount_share = [MountEntry(name="tmpfs", mount="/run/snapd/ns", tp="tmpfs",
                                      options="rw,nosuid,nodev,seclabel,mode=755")]
    current_actor_context.feed(StorageInfo(mount=without_mount_share))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/common/actors/checknonmountboots390/libraries/checknonmountboots390.py

import os

from leapp import reporting
from leapp.libraries.common.config import architecture


def perform_check():
    if not architecture.matches_architecture(architecture.ARCH_S390X):
        return

    if os.path.ismount('/boot'):
        return

    data = [
        reporting.Title('Leapp detected known issue related to /boot on s390x architecture'),
        reporting.Summary((
            'Due to a bug in the Leapp code, there is a situation when the upgrade process'
            ' removes content of /boot when the directory is not on a separate partition and'
            ' the system is running on S390x architecture. To avoid this from happening, we'
            ' are inhibiting the upgrade process in this release until the issue has been fixed.'
        )),
        reporting.Groups([reporting.Groups.INHIBITOR]),
        reporting.Groups([reporting.Groups.FILESYSTEM, reporting.Groups.UPGRADE_PROCESS, reporting.Groups.BOOT]),
        reporting.Severity(reporting.Severity.HIGH),
    ]

    reporting.create_report(data)


# repos/system_upgrade/common/actors/checknonmountboots390/tests/test_checknonmountboots390.py

import pytest

from leapp.libraries.actor import checknonmountboots390


class CheckNonMountBootS390ReportCreated(Exception):
    pass


@pytest.mark.parametrize(
    'matches_arch,ismount,should_report', (
        (True, True, False),
        (True, False, True),
        (False, True, False),
        (False, False, False),
    )
)
def test_checknonmountboots390_perform_check(monkeypatch, matches_arch, ismount, should_report):
    def _create_report(data):
        raise CheckNonMountBootS390ReportCreated()

    monkeypatch.setattr(checknonmountboots390.architecture, 'matches_architecture', lambda x: matches_arch)
    monkeypatch.setattr(checknonmountboots390.os.path, 'ismount', lambda x: ismount)
    monkeypatch.setattr(checknonmountboots390.reporting, 'create_report', _create_report)

    if should_report:
        with pytest.raises(CheckNonMountBootS390ReportCreated):
            checknonmountboots390.perform_check()
    else:
        checknonmountboots390.perform_check()


# repos/system_upgrade/common/actors/checkosrelease/libraries/checkosrelease.py

import os

from leapp import reporting
from leapp.libraries.common.config import version

COMMON_REPORT_TAGS = [reporting.Groups.SANITY]

related = [reporting.RelatedResource('file', '/etc/os-release')]


def skip_check():
    """ Check if an environment variable was used to skip this actor """
    if os.getenv('LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE'):
        reporting.create_report([
            reporting.Title('Skipped OS release check'),
            reporting.Summary('Source RHEL release check skipped via LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE env var.'),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups(COMMON_REPORT_TAGS)
        ] + related)

        return True
    return False


def check_os_version():
    """ Check the RHEL minor version and inhibit the upgrade if it does not match the supported ones """
    if not version.is_supported_version():
        supported_releases = []
        for rel in version.SUPPORTED_VERSIONS:
            for ver in version.SUPPORTED_VERSIONS[rel]:
                supported_releases.append(rel.upper() + ' ' + ver)
        reporting.create_report([
            reporting.Title(
                'The installed OS version is not supported for the in-place upgrade to the target RHEL version'
            ),
            reporting.Summary(
                'The supported OS releases for the upgrade process:\n'
                ' {}'.format('\n'.join(supported_releases))
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups(COMMON_REPORT_TAGS),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            # we want to set a static Key here because of different Title per path
            reporting.Key('1c7a98849a747ec9890f04bf4321de7280970715')
        ] + related)


# repos/system_upgrade/common/actors/checkosrelease/tests/test_checkosrelease.py

import os

from leapp import reporting
from leapp.libraries.actor import checkosrelease
from leapp.libraries.common.config import version
from leapp.libraries.common.testutils import create_report_mocked, produce_mocked
from leapp.utils.report import is_inhibitor


def test_skip_check(monkeypatch):
    monkeypatch.setenv('LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE', '1')
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    assert checkosrelease.skip_check()
    assert reporting.create_report.called == 1
    assert 'Skipped OS release check' in reporting.create_report.report_fields['title']
    assert reporting.create_report.report_fields['severity'] == 'high'


def test_no_skip_check(monkeypatch):
    monkeypatch.delenv('LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE', raising=False)
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    assert not checkosrelease.skip_check()
    assert reporting.create_report.called == 0


def test_not_supported_release(monkeypatch):
    monkeypatch.setattr(version, "is_supported_version", lambda: False)
    monkeypatch.setattr(version, "get_source_major_version", lambda: '7')
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    checkosrelease.check_os_version()
    assert reporting.create_report.called == 1
    assert 'The installed OS version is not supported' in reporting.create_report.report_fields['title']
    assert is_inhibitor(reporting.create_report.report_fields)


def test_supported_release(monkeypatch):
    monkeypatch.setattr(version, "is_supported_version", lambda: True)
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    checkosrelease.check_os_version()
    assert reporting.create_report.called == 0


# repos/system_upgrade/common/actors/checkpersistentmounts/libraries/checkpersistentmounts.py

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


def inhibit_upgrade_due_non_persistent_mount(mountpoint):
    summary = (
        'Leapp detected that the {0} mountpoint is mounted, with no corresponding entry in /etc/fstab. '
        'The upgrade process cannot continue with {0} mounted in non-persistent fashion, '
        'as Leapp needs this mount to be available after a reboot.'
    )

    hint = (
        'Add {0} mount entry to /etc/fstab'
    )

    reporting.create_report([
        reporting.Title(
            'Detected partitions mounted in a non-persistent fashion, preventing a successful in-place upgrade.'
        ),
        reporting.Summary(summary.format(mountpoint)),
        reporting.Remediation(hint=hint.format(mountpoint)),
        reporting.RelatedResource('file', '/etc/fstab'),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Groups([reporting.Groups.FILESYSTEM]),
        reporting.Groups([reporting.Groups.INHIBITOR]),
    ])


def check_mount_is_persistent(storage_info, mountpoint):
    """Check if mountpoint is mounted in persistent fashion"""

    mount_entry_exists = any(me.mount == mountpoint for me in storage_info.mount)
    fstab_entry_exists = any(fe.fs_file == mountpoint for fe in storage_info.fstab)

    if mount_entry_exists and not fstab_entry_exists:
        inhibit_upgrade_due_non_persistent_mount(mountpoint)


def check_persistent_mounts():
    storage_info = next(api.consume(StorageInfo), None)
    if storage_info:
        check_mount_is_persistent(storage_info, '/var/lib/leapp')


# repos/system_upgrade/common/actors/checkpersistentmounts/tests/test_checkpersistentmounts.py

import pytest

from leapp import reporting
from leapp.libraries.actor.checkpersistentmounts import check_persistent_mounts
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import FstabEntry, MountEntry, StorageInfo

MOUNT_ENTRY = MountEntry(name='/dev/sdaX', tp='ext4', mount='/var/lib/leapp', options='defaults')

FSTAB_ENTRY = FstabEntry(fs_spec='', fs_file='/var/lib/leapp', fs_vfstype='',
                         fs_mntops='defaults', fs_freq='0', fs_passno='0')


@pytest.mark.parametrize(
    ('storage_info', 'should_inhibit'),
    [
        (
            StorageInfo(mount=[MOUNT_ENTRY], fstab=[]),
            True
        ),
        (
            StorageInfo(mount=[], fstab=[FSTAB_ENTRY]),
            False
        ),
        (
            StorageInfo(mount=[MOUNT_ENTRY], fstab=[FSTAB_ENTRY]),
            False
        ),
    ]
)
def test_var_lib_leapp_non_persistent_is_detected(monkeypatch, storage_info, should_inhibit):

    created_reports = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[storage_info]))
    monkeypatch.setattr(reporting, 'create_report', created_reports)

    check_persistent_mounts()

    assert bool(created_reports.called) == should_inhibit


# repos/system_upgrade/common/actors/checkrhsmsku/libraries/checkrhsmsku.py

from leapp import reporting
from leapp.libraries.common import rhsm
from leapp.libraries.stdlib import api
from leapp.models import RHSMInfo
from leapp.reporting import create_report

SCA_TEXT = "Content Access Mode is set to Simple Content Access"


def process():
    if not rhsm.skip_rhsm():
        for info in api.consume(RHSMInfo):
            if not info.attached_skus and not info.sca_detected:
                create_report([
                    reporting.Title('The system is not registered or subscribed.'),
                    reporting.Summary(
                        'The system has to be registered and subscribed to be able to proceed'
                        ' with the upgrade, unless the --no-rhsm option is specified when'
                        ' executing leapp.'
                    ),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Groups([reporting.Groups.SANITY]),
                    reporting.Groups([reporting.Groups.INHIBITOR]),
                    reporting.Remediation(
                        hint='Register your system with the subscription-manager tool and attach'
                             ' proper SKUs to be able to proceed the upgrade or use the --no-rhsm'
                             ' leapp option if you want to provide target repositories by yourself.'),
                    reporting.RelatedResource('package', 'subscription-manager')
                ])


# repos/system_upgrade/common/actors/checkrhsmsku/tests/test_rhsmsku_checkrhsmsku.py

from leapp.libraries.actor import checkrhsmsku
from leapp.libraries.common import rhsm
from leapp.libraries.common.testutils import create_report_mocked
from leapp.libraries.stdlib import api
from leapp.models import RHSMInfo
from leapp.utils.report import is_inhibitor


def test_sku_report_skipped(monkeypatch):
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: True)
    monkeypatch.setattr(api, 'consume', lambda x: (RHSMInfo(attached_skus=[]),))
    monkeypatch.setattr(checkrhsmsku, 'create_report', create_report_mocked())
    checkrhsmsku.process()
    assert not checkrhsmsku.create_report.called


def test_sku_report_has_skus(monkeypatch):
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    monkeypatch.setattr(api, 'consume', lambda x: (RHSMInfo(attached_skus=['testing-sku']),))
    monkeypatch.setattr(checkrhsmsku, 'create_report', create_report_mocked())
    checkrhsmsku.process()
    assert not checkrhsmsku.create_report.called


def test_sku_report_has_no_skus(monkeypatch):
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    monkeypatch.setattr(api, 'consume', lambda x: (RHSMInfo(attached_skus=[]),))
    monkeypatch.setattr(checkrhsmsku, 'create_report', create_report_mocked())
    checkrhsmsku.process()
    assert checkrhsmsku.create_report.called == 1
    assert checkrhsmsku.create_report.report_fields['title'] == 'The system is not registered or subscribed.'
    assert checkrhsmsku.create_report.report_fields['severity'] == 'high'
    assert is_inhibitor(checkrhsmsku.create_report.report_fields)


def test_sku_report_has_sca(monkeypatch):
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    monkeypatch.setattr(api, 'consume', lambda x: (RHSMInfo(attached_skus=[], sca_detected=True),))
    monkeypatch.setattr(checkrhsmsku, 'create_report', create_report_mocked())
    checkrhsmsku.process()
    assert not checkrhsmsku.create_report.called


# repos/system_upgrade/common/actors/checkrootsymlinks/tests/unit_test_checkrootsymlinks.py

import pytest
import six

from leapp.models import InvalidRootSubdirectory, Report, RootDirectory, RootSubdirectory
from leapp.snactor.fixture import current_actor_context


@pytest.mark.parametrize('modified,invalid,expected_report', [
    ({}, {}, False),
    ({'bin': '/usr/bin'}, {}, True),
    ({'sbin': 'usr/bin'}, {}, False),
    ({'sbin': '/usr/sbin'}, {}, True),
    ({'lib': '/usr/lib', 'lib64': 'usr/lib64'}, {}, True),
    ({}, {b'\xc4\xc9\xd2\xc5\xcb\xd4\xcf\xd2\xc9\xd1': b''}, False),
    ({}, {b'\xc4\xc9\xd2\xc5\xcb\xd4\xcf\xd2\xc9\xd1': b'usr/bin'}, False),
    ({'lib': '/usr/lib', 'lib64': 'usr/lib64'}, {b'\xc4\xc9\xd2\xc5\xcb\xd4\xcf\xd2\xc9\xd1': b'/usr/lib64'}, True),
    ({}, {b'\xc4\xc9\xd2\xc5\xcb\xd4\xcf\xd2\xc9\xd1': b'/usr/lib64'}, True)])
def test_wrong_symlink_inhibitor(current_actor_context, modified, invalid, expected_report):
    invalid_subdirs = {}
    subdirs = {
        'bin': 'usr/bin',
        'boot': '',
        'dev': '',
        'etc': '',
        'home': '',
        'lib': 'usr/lib',
        'lib64': 'usr/lib64',
        'media': '',
        'mnt': '',
        'opt': '',
        'proc': '',
        'root': '',
        'run': '',
        'sbin': 'usr/sbin',
        'srv': '',
        'sys': '',
        'tmp': '',
        'usr': '',
        'var': ''
    }
    subdirs.update(modified)
    invalid_subdirs.update(invalid)

    items = [RootSubdirectory(name=name, target=target) for name, target in subdirs.items()]
    invalid_items = [InvalidRootSubdirectory(name=name, target=target) for name, target in invalid_subdirs.items()]
    current_actor_context.feed(RootDirectory(items=items, invalid_items=invalid_items))
    current_actor_context.run()
    if expected_report:
        report = current_actor_context.consume(Report)
        assert report
        # Make sure the hint is there in case of non-utf bad symlinks
        if invalid_subdirs:
            msg = 'symbolic links point to absolute paths that have non-utf8 encoding'
            hint = next((rem['context'] for rem in report[0].report['detail']['remediations']
                         if rem['type'] == 'hint'), None)
            assert msg in hint
    else:
        assert not current_actor_context.consume(Report)


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

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

# 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
# RHEL 8.2 target requirements
SAP_HANA_RHEL82_REQUIRED_PATCH_LEVELS = ((5, 54, 0),)
SAP_HANA_RHEL82_MINIMAL_VERSION_STRING = 'HANA 2.0 SPS05 rev 54 or later'

# RHEL 8.6 target requirements
SAP_HANA_RHEL86_REQUIRED_PATCH_LEVELS = ((5, 59, 2),)
SAP_HANA_RHEL86_MINIMAL_VERSION_STRING = 'HANA 2.0 SPS05 rev 59.02 or later'

# RHEL 9 target requirements
SAP_HANA_RHEL9_REQUIRED_PATCH_LEVELS = ((5, 59, 4), (6, 63, 0))
SAP_HANA_RHEL9_MINIMAL_VERSION_STRING = 'HANA 2.0 SPS05 rev 59.04 or later, or SPS06 rev 63 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.Groups([reporting.Groups.SANITY]),
            reporting.Groups([reporting.Groups.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 _min_ver_string():
    if version.get_target_major_version() == '8':
        ver_str = SAP_HANA_RHEL86_MINIMAL_VERSION_STRING
        if version.matches_target_version('8.2'):
            ver_str = SAP_HANA_RHEL82_MINIMAL_VERSION_STRING
    else:
        ver_str = SAP_HANA_RHEL9_MINIMAL_VERSION_STRING
    return ver_str


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:
        min_ver_string = _min_ver_string()
        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=min_ver_string))),
            reporting.ExternalLink(url='https://launchpad.support.sap.com/#/notes/2235581',
                                   title='SAP HANA: Supported Operating Systems'),
            reporting.Groups([reporting.Groups.SANITY]),
            reporting.Groups([reporting.Groups.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, patchlevels):
    """ 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 patchlevels]
        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 patchlevels:
            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 fulfills the minimal requirements for the target RHEL """
    if version.get_target_major_version() == '8':
        patchlevels = SAP_HANA_RHEL86_REQUIRED_PATCH_LEVELS
        if version.matches_target_version('8.2'):
            patchlevels = SAP_HANA_RHEL82_REQUIRED_PATCH_LEVELS
    else:
        patchlevels = SAP_HANA_RHEL9_REQUIRED_PATCH_LEVELS
    return _major_version_check(instance) and _sp_rev_patchlevel_check(instance, patchlevels)


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:
        min_ver_string = _min_ver_string()
        detected = _create_detected_instances_list(found)
        reporting.create_report([
            reporting.Title('SAP HANA needs to be updated before the RHEL 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 installed with a lower version'
                 ' than required on the target system:\n'
                 '{detected}').format(detected=detected, min_hana_version=min_ver_string)
            ),
            reporting.RemediationHint('Update SAP HANA at least to {}'.format(min_ver_string)),
            reporting.ExternalLink(url='https://launchpad.support.sap.com/#/notes/2235581',
                                   title='SAP HANA: Supported Operating Systems'),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.SANITY]),
            reporting.Groups([reporting.Groups.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):
        if version.get_target_major_version() == '8':
            elink = 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')
        else:
            elink = reporting.ExternalLink(
                url='https://access.redhat.com/solutions/6980855',
                title='How to in-place upgrade SAP environments from RHEL 8 to RHEL 9')

        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.Groups([reporting.Groups.SANITY]),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Audience('sysadmin'),
            elink,
        ])
        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/common/actors/checksaphana/tests/test_checksaphana.py

import pytest

from leapp.libraries.actor import checksaphana
from leapp.libraries.common import testutils
from leapp.libraries.common.config import version
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 the RHEL 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_rhel82_hana_min_version(monkeypatch, major, rev, patchlevel, result):
    monkeypatch.setattr(version, 'get_target_major_version', lambda: '8')
    monkeypatch.setattr(version, 'get_target_version', lambda: '8.2')
    monkeypatch.setattr(checksaphana, 'SAP_HANA_RHEL82_REQUIRED_PATCH_LEVELS', ((4, 48, 2), (5, 52, 0)))
    assert checksaphana._fullfills_hana_min_version(
        MockSAPHanaVersionInstance(
            major=major,
            rev=rev,
            patchlevel=patchlevel,
        )
    ) == result


@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_rhel86_hana_min_version(monkeypatch, major, rev, patchlevel, result):
    monkeypatch.setattr(version, 'get_target_major_version', lambda: '8')
    monkeypatch.setattr(version, 'get_target_version', lambda: '8.6')
    monkeypatch.setattr(checksaphana, 'SAP_HANA_RHEL86_REQUIRED_PATCH_LEVELS', ((4, 48, 2), (5, 52, 0)))
    assert checksaphana._fullfills_hana_min_version(
        MockSAPHanaVersionInstance(
            major=major,
            rev=rev,
            patchlevel=patchlevel,
        )
    ) == result


@pytest.mark.parametrize(
    'major,rev,patchlevel,result', (
        (2, 59, 4, True),
        (2, 59, 5, True),
        (2, 59, 6, True),
        (2, 60, 0, False),
        (2, 61, 0, False),
        (2, 62, 0, False),
        (2, 63, 2, True),
        (2, 48, 1, False),
        (2, 48, 0, False),
        (2, 59, 0, False),
        (2, 59, 1, False),
        (2, 59, 2, False),
        (2, 59, 3, False),
        (2, 38, 2, False),
        (2, 64, 0, True),
    )
)
def test_checksaphana__fullfills_hana_rhel9_min_version(monkeypatch, major, rev, patchlevel, result):
    monkeypatch.setattr(version, 'get_target_major_version', lambda: '9')
    monkeypatch.setattr(version, 'get_target_version', lambda: '9.0')
    monkeypatch.setattr(checksaphana, 'SAP_HANA_RHEL9_REQUIRED_PATCH_LEVELS', ((5, 59, 4), (6, 63, 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_RHEL86_REQUIRED_PATCH_LEVELS', ((4, 48, 2), (5, 52, 0)))
    monkeypatch.setattr(version, 'get_target_major_version', lambda: '8')
    monkeypatch.setattr(version, 'get_target_version', lambda: '8.6')
    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/common/actors/checkselinux/libraries/checkselinux.py

from leapp import reporting
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api
from leapp.models import KernelCmdlineArg, SELinuxFacts, SelinuxPermissiveDecision, SelinuxRelabelDecision

DOC_URL = 'https://red.ht/rhel9-disabling-selinux'


def process():
    facts = next(api.consume(SELinuxFacts), None)
    if not facts:
        return

    enabled = facts.enabled
    conf_status = facts.static_mode

    if conf_status == 'disabled':
        if get_target_major_version() == '9':
            api.produce(KernelCmdlineArg(key='selinux', value='0'))
            reporting.create_report([
                reporting.Title('LEAPP detected SELinux disabled in "/etc/selinux/config"'),
                reporting.Summary(
                    'On RHEL 9, disabling SELinux in "/etc/selinux/config" is no longer possible. '
                    'This way, the system starts with SELinux enabled but with no policy loaded. LEAPP '
                    'will automatically disable SELinux using "SELINUX=0" kernel command line parameter. '
                    'However, Red Hat strongly recommends to have SELinux enabled'
                ),
                reporting.Severity(reporting.Severity.INFO),
                reporting.Groups([reporting.Groups.SELINUX]),
                reporting.RelatedResource('file', '/etc/selinux/config'),
                reporting.ExternalLink(url=DOC_URL, title='Disabling SELinux'),
            ])

        if enabled:
            reporting.create_report([
                reporting.Title('SElinux should be disabled based on the configuration file but it is enabled'),
                reporting.Summary(
                    'This message is to inform user about non-standard SElinux configuration. Please check '
                    '"/etc/selinux/config" to see whether the configuration is set as expected.'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Groups([reporting.Groups.SELINUX, reporting.Groups.SECURITY])
            ])
        reporting.create_report([
            reporting.Title('SElinux disabled'),
            reporting.Summary('SElinux disabled, continuing...'),
            reporting.Groups([reporting.Groups.SELINUX, reporting.Groups.SECURITY])
        ])
        return

    if conf_status in ('enforcing', 'permissive'):
        api.produce(SelinuxRelabelDecision(set_relabel=True))
        reporting.create_report([
            reporting.Title('SElinux relabeling will be scheduled'),
            reporting.Summary('SElinux relabeling will be scheduled as the status is permissive/enforcing.'),
            reporting.Severity(reporting.Severity.INFO),
            reporting.Groups([reporting.Groups.SELINUX, reporting.Groups.SECURITY])
        ])

    if conf_status == 'enforcing':
        api.produce(SelinuxPermissiveDecision(
            set_permissive=True))
        reporting.create_report([
            reporting.Title('SElinux will be set to permissive mode'),
            reporting.Summary(
                'SElinux will be set to permissive mode. Current mode: enforcing. This action is '
                'required by the upgrade process to make sure the upgraded system can boot without '
                'beinig blocked by SElinux rules.'
            ),
            reporting.Severity(reporting.Severity.LOW),
            reporting.Remediation(hint=(
                'Make sure there are no SElinux related warnings after the upgrade and enable SElinux '
                'manually afterwards. Notice: You can ignore the "/root/tmp_leapp_py3" SElinux warnings.'
                )
            ),
            reporting.Groups([reporting.Groups.SELINUX, reporting.Groups.SECURITY])
        ])


# repos/system_upgrade/common/actors/checkselinux/tests/test_checkselinux.py

import pytest

from leapp import reporting
from leapp.libraries.actor import checkselinux
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import Report, SELinuxFacts, SelinuxPermissiveDecision, SelinuxRelabelDecision
from leapp.snactor.fixture import current_actor_context


def create_selinuxfacts(static_mode, enabled, policy='targeted', mls_enabled=True):
    runtime_mode = static_mode if static_mode != 'disabled' else None

    return SELinuxFacts(
        runtime_mode=runtime_mode,
        static_mode=static_mode,
        enabled=enabled,
        policy=policy,
        mls_enabled=mls_enabled
    )


@pytest.mark.parametrize('mode', ('permissive', 'enforcing'))
def test_actor_schedule_relabelling(monkeypatch, mode):

    fact = create_selinuxfacts(static_mode=mode, enabled=True)

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[fact]))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    checkselinux.process()

    assert api.produce.model_instances[0].set_relabel
    assert reporting.create_report.called


def test_actor_set_permissive(monkeypatch):
    relabel = create_selinuxfacts(static_mode='enforcing', enabled=True)

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[relabel]))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    checkselinux.process()

    assert api.produce.model_instances[0].set_relabel
    assert api.produce.model_instances[1].set_permissive
    assert reporting.create_report.called


@pytest.mark.parametrize('el8_to_el9', (True, False))
def test_actor_selinux_disabled(monkeypatch, el8_to_el9):
    disabled = create_selinuxfacts(enabled=False, static_mode='disabled')

    target_ver = '8' if not el8_to_el9 else '9'

    monkeypatch.setattr(checkselinux, 'get_target_major_version', lambda: target_ver)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[disabled]))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    checkselinux.process()
    if el8_to_el9:
        assert api.produce.model_instances[0]
        assert reporting.create_report.called == 2
    else:
        assert not api.produce.model_instances
        assert reporting.create_report.called


# repos/system_upgrade/common/actors/checkskippedrepositories/tests/test_checkskippedrepos_checkskippedrepositories.py

from leapp.models import Report, SkippedRepositories


def test_skipped_repos(current_actor_context):
    reported_packages = ['pkg_a', 'pkg_b', 'pkg_c']
    reported_repos = ['repo_a', 'repo_b', 'repo_c']
    current_actor_context.feed(
        SkippedRepositories(
            packages=list(reported_packages),
            repos=list(reported_repos)
        )
    )

    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports
    assert len(reports) == 1
    report_fields = reports[0].report
    for pkg in reported_packages:
        assert '\n- {}'.format(pkg) in report_fields['summary']
    for repo in reported_repos:
        assert '\n- {}'.format(repo) in report_fields['summary']


def test_skipped_just_repos(current_actor_context):
    reported_repos = ['repo_a', 'repo_b', 'repo_c']
    current_actor_context.feed(
        SkippedRepositories(
            packages=[],
            repos=list(reported_repos)
        )
    )

    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert reports
    assert len(reports) == 1
    report_fields = reports[0].report
    for repo in reported_repos:
        assert '\n- {}'.format(repo) in report_fields['summary']


def test_skipped_repos_empty(current_actor_context):
    current_actor_context.feed(
        SkippedRepositories(
            packages=[],
            repos=[]
        )
    )
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert not reports


def test_skipped_repos_no_repos(current_actor_context):
    current_actor_context.feed(
        SkippedRepositories(
            packages=['woot'],
            repos=[]
        )
    )
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert not reports


def test_skipped_repos_no_message(current_actor_context):
    current_actor_context.run()
    reports = list(current_actor_context.consume(Report))
    assert not reports


# repos/system_upgrade/common/actors/checksystemarch/libraries/checksystemarch.py

from leapp import reporting
from leapp.libraries.common.config import architecture


def check_architecture():
    """Check if given architecture is supported by upgrade process"""
    if not architecture.matches_architecture(*architecture.ARCH_SUPPORTED):
        inhibit_upgrade()


def inhibit_upgrade():
    """Generate an upgrade inhibitor"""
    reporting.create_report(
        [
            reporting.Title('Unsupported architecture'),
            reporting.Summary(
                'Upgrade process is only supported on {} systems.'.format(
                    ', '.join(architecture.ARCH_SUPPORTED)
                )
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.SANITY]),
            reporting.Groups([reporting.Groups.INHIBITOR]),
        ]
    )


# repos/system_upgrade/common/actors/checksystemarch/tests/test_checksystemarch.py

from leapp import reporting
from leapp.libraries.actor import checksystemarch
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.utils.report import is_inhibitor


def test_valid_architectures(monkeypatch):
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=architecture.ARCH_ACCEPTED[0]))

    checksystemarch.check_architecture()

    assert reporting.create_report.called == 0


def test_invalid_architecture(monkeypatch):
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='invalid_architecture'))

    checksystemarch.check_architecture()
    assert reporting.create_report.called == 1
    assert 'Unsupported architecture' in reporting.create_report.report_fields['title']
    assert 'Upgrade process is only supported' in reporting.create_report.report_fields['summary']
    assert reporting.create_report.report_fields['severity'] == 'high'
    assert is_inhibitor(reporting.create_report.report_fields)


# repos/system_upgrade/common/actors/checktargetiso/libraries/check_target_iso.py

import os

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import version
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import StorageInfo, TargetOSInstallationImage


def inhibit_if_not_valid_iso_file(iso):
    inhibit_title = None
    target_os = 'RHEL {}'.format(version.get_target_major_version())
    if not os.path.exists(iso.path):
        inhibit_title = 'Provided {target_os} installation ISO does not exists.'.format(target_os=target_os)
        inhibit_summary_tpl = 'The supplied {target_os} ISO path \'{iso_path}\' does not point to an existing file.'
        inhibit_summary = inhibit_summary_tpl.format(target_os=target_os, iso_path=iso.path)
    else:
        try:
            # TODO(mhecko): Figure out whether we will keep this since the scan actor is mounting the ISO anyway
            file_cmd_output = run(['file', '--mime', iso.path])
            if 'application/x-iso9660-image' not in file_cmd_output['stdout']:
                inhibit_title = 'Provided {target_os} installation image is not a valid ISO.'.format(
                        target_os=target_os)
                summary_tpl = ('The provided {target_os} installation image path \'{iso_path}\''
                               'does not point to a valid ISO image.')
                inhibit_summary = summary_tpl.format(target_os=target_os, iso_path=iso.path)

        except CalledProcessError as err:
            raise StopActorExecutionError(message='Failed to check whether {0} is an ISO file.'.format(iso.path),
                                          details={'details': '{}'.format(err)})
    if inhibit_title:
        remediation_hint = ('Check whether the supplied target OS installation path points to a valid'
                            '{target_os} ISO image.'.format(target_os=target_os))

        reporting.create_report([
            reporting.Title(inhibit_title),
            reporting.Summary(inhibit_summary),
            reporting.Remediation(hint=remediation_hint),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.REPOSITORY]),
        ])
        return True
    return False


def inhibit_if_failed_to_mount_iso(iso):
    if iso.was_mounted_successfully:
        return False

    target_os = 'RHEL {0}'.format(version.get_target_major_version())
    title = 'Failed to mount the provided {target_os} installation image.'
    summary = 'The provided {target_os} installation image {iso_path} could not be mounted.'
    hint = 'Verify that the provided ISO is a valid {target_os} installation image'
    reporting.create_report([
        reporting.Title(title.format(target_os=target_os)),
        reporting.Summary(summary.format(target_os=target_os, iso_path=iso.path)),
        reporting.Remediation(hint=hint.format(target_os=target_os)),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Groups([reporting.Groups.INHIBITOR]),
        reporting.Groups([reporting.Groups.REPOSITORY]),
    ])
    return True


def inhibit_if_wrong_iso_rhel_version(iso):
    # If the major version could not be determined, the iso.rhel_version will be an empty string
    if not iso.rhel_version:
        reporting.create_report([
            reporting.Title(
                'Failed to determine RHEL version provided by the supplied installation image.'),
            reporting.Summary(
                'Could not determine what RHEL version does the supplied installation image'
                ' located at {iso_path} provide.'.format(iso_path=iso.path)
            ),
            reporting.Remediation(hint='Check that the supplied image is a valid RHEL installation image.'),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.REPOSITORY]),
        ])
        return

    iso_rhel_major_version = iso.rhel_version.split('.')[0]
    req_major_ver = version.get_target_major_version()
    if iso_rhel_major_version != req_major_ver:
        summary = ('The provided RHEL installation image provides RHEL {iso_rhel_ver}, however, a RHEL '
                   '{required_rhel_ver} image is required for the upgrade.')

        reporting.create_report([
            reporting.Title('The provided installation image provides invalid RHEL version.'),
            reporting.Summary(summary.format(iso_rhel_ver=iso.rhel_version, required_rhel_ver=req_major_ver)),
            reporting.Remediation(hint='Check that the supplied image is a valid RHEL installation image.'),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.REPOSITORY]),
        ])


def inhibit_if_iso_not_located_on_persistent_partition(iso):
    # Check whether the filesystem that on which the ISO resides is mounted in a persistent fashion
    storage_info = next(api.consume(StorageInfo), None)
    if not storage_info:
        raise StopActorExecutionError('Actor did not receive any StorageInfo message.')

    # Assumes that the path has been already checked for validity, e.g., the ISO path points to a file
    iso_mountpoint = iso.path
    while not os.path.ismount(iso_mountpoint):  # Guaranteed to terminate because we must reach / eventually
        iso_mountpoint = os.path.dirname(iso_mountpoint)

    is_iso_on_persistent_partition = False
    for fstab_entry in storage_info.fstab:
        if fstab_entry.fs_file == iso_mountpoint:
            is_iso_on_persistent_partition = True
            break

    if not is_iso_on_persistent_partition:
        target_ver = version.get_target_major_version()
        title = 'The RHEL {target_ver} installation image is not located on a persistently mounted partition'
        summary = ('The provided RHEL {target_ver} installation image {iso_path} is located'
                   ' on a partition without an entry in /etc/fstab, causing the partition '
                   ' to be persistently mounted.')
        hint = ('Move the installation image to a partition that is persistently mounted, or create an /etc/fstab'
                ' entry for the partition on which the installation image is located.')

        reporting.create_report([
            reporting.Title(title.format(target_ver=target_ver)),
            reporting.Summary(summary.format(target_ver=target_ver, iso_path=iso.path)),
            reporting.Remediation(hint=hint),
            reporting.RelatedResource('file', '/etc/fstab'),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.REPOSITORY]),
        ])


def inihibit_if_iso_does_not_contain_basic_repositories(iso):
    missing_basic_repoids = {'BaseOS', 'AppStream'}

    for custom_repo in iso.repositories:
        missing_basic_repoids.remove(custom_repo.repoid)
        if not missing_basic_repoids:
            break

    if missing_basic_repoids:
        target_ver = version.get_target_major_version()

        title = 'Provided RHEL {target_ver} installation ISO is missing fundamental repositories.'
        summary = ('The supplied RHEL {target_ver} installation ISO {iso_path} does not contain '
                   '{missing_repos} repositor{suffix}')
        hint = 'Check whether the supplied ISO is a valid RHEL {target_ver} installation image.'

        reporting.create_report([
            reporting.Title(title.format(target_ver=target_ver)),
            reporting.Summary(summary.format(target_ver=target_ver,
                                             iso_path=iso.path,
                                             missing_repos=','.join(missing_basic_repoids),
                                             suffix=('y' if len(missing_basic_repoids) == 1 else 'ies'))),
            reporting.Remediation(hint=hint.format(target_ver=target_ver)),
            reporting.Severity(reporting.Severity.MEDIUM),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.REPOSITORY]),
        ])


def perform_target_iso_checks():
    requested_target_iso_msg_iter = api.consume(TargetOSInstallationImage)
    target_iso = next(requested_target_iso_msg_iter, None)

    if not target_iso:
        return

    if next(requested_target_iso_msg_iter, None):
        api.current_logger().warn('Received multiple msgs with target ISO to use.')

    # Cascade the inhibiting conditions so that we do not spam the user with inhibitors
    is_iso_invalid = inhibit_if_not_valid_iso_file(target_iso)
    if not is_iso_invalid:
        failed_to_mount_iso = inhibit_if_failed_to_mount_iso(target_iso)
        if not failed_to_mount_iso:
            inhibit_if_wrong_iso_rhel_version(target_iso)
            inhibit_if_iso_not_located_on_persistent_partition(target_iso)
            inihibit_if_iso_does_not_contain_basic_repositories(target_iso)


# repos/system_upgrade/common/actors/checktargetiso/tests/test_check_target_iso.py

import os

import pytest

from leapp import reporting
from leapp.libraries.actor import check_target_iso
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepository, FstabEntry, StorageInfo, TargetOSInstallationImage
from leapp.utils.report import is_inhibitor


@pytest.mark.parametrize('mount_successful', (True, False))
def test_inhibit_on_iso_mount_failure(monkeypatch, mount_successful):
    create_report_mock = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', create_report_mock)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())

    target_iso_msg = TargetOSInstallationImage(path='',
                                               mountpoint='',
                                               repositories=[],
                                               was_mounted_successfully=mount_successful)

    check_target_iso.inhibit_if_failed_to_mount_iso(target_iso_msg)

    expected_report_count = 0 if mount_successful else 1
    assert create_report_mock.called == expected_report_count
    if not mount_successful:
        assert is_inhibitor(create_report_mock.reports[0])


@pytest.mark.parametrize(('detected_iso_rhel_ver', 'required_target_ver', 'should_inhibit'),
                         (('8.6', '8.6', False), ('7.9', '8.6', True), ('8.5', '8.6', False), ('', '8.6', True)))
def test_inhibit_on_detected_rhel_version(monkeypatch, detected_iso_rhel_ver, required_target_ver, should_inhibit):
    create_report_mock = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', create_report_mock)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver=required_target_ver))

    target_iso_msg = TargetOSInstallationImage(path='',
                                               mountpoint='',
                                               repositories=[],
                                               rhel_version=detected_iso_rhel_ver,
                                               was_mounted_successfully=True)

    check_target_iso.inhibit_if_wrong_iso_rhel_version(target_iso_msg)

    expected_report_count = 1 if should_inhibit else 0
    assert create_report_mock.called == expected_report_count
    if should_inhibit:
        assert is_inhibitor(create_report_mock.reports[0])


@pytest.mark.parametrize(('iso_repoids', 'should_inhibit'),
                         ((('BaseOS', 'AppStream'), False), (('BaseOS',), True), (('AppStream',), True), ((), True)))
def test_inhibit_on_invalid_rhel_version(monkeypatch, iso_repoids, should_inhibit):
    create_report_mock = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', create_report_mock)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())

    iso_repositories = [CustomTargetRepository(repoid=repoid, baseurl='', name='') for repoid in iso_repoids]

    target_iso_msg = TargetOSInstallationImage(path='',
                                               mountpoint='',
                                               repositories=iso_repositories,
                                               was_mounted_successfully=True)

    check_target_iso.inihibit_if_iso_does_not_contain_basic_repositories(target_iso_msg)

    expected_report_count = 1 if should_inhibit else 0
    assert create_report_mock.called == expected_report_count
    if should_inhibit:
        assert is_inhibitor(create_report_mock.reports[0])


def test_inhibit_on_nonexistent_iso(monkeypatch):
    iso_path = '/nonexistent/iso'
    create_report_mock = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', create_report_mock)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())

    def mocked_os_path_exists(path):
        assert path == iso_path, 'The actor should check only the path to ISO for existence.'
        return False

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

    target_iso_msg = TargetOSInstallationImage(path=iso_path,
                                               mountpoint='',
                                               repositories=[],
                                               was_mounted_successfully=True)

    check_target_iso.inhibit_if_not_valid_iso_file(target_iso_msg)

    assert create_report_mock.called == 1
    assert is_inhibitor(create_report_mock.reports[0])


@pytest.mark.parametrize(('filetype', 'should_inhibit'),
                         (('{path}: text/plain; charset=us-ascii', True),
                          ('{path}: application/x-iso9660-image; charset=binary', False)))
def test_inhibit_on_path_not_pointing_to_iso(monkeypatch, filetype, should_inhibit):
    iso_path = '/path/not-an-iso'
    create_report_mock = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', create_report_mock)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())

    def mocked_os_path_exists(path):
        assert path == iso_path, 'The actor should check only the path to ISO for existence.'
        return True

    def mocked_run(cmd, *args, **kwargs):
        assert cmd[0] == 'file', 'The actor should only use `file` cmd when checking for file type.'
        return {'stdout': filetype.format(path=iso_path)}

    monkeypatch.setattr(os.path, 'exists', mocked_os_path_exists)
    monkeypatch.setattr(check_target_iso, 'run', mocked_run)

    target_iso_msg = TargetOSInstallationImage(path=iso_path, mountpoint='', repositories=[])

    check_target_iso.inhibit_if_not_valid_iso_file(target_iso_msg)

    if should_inhibit:
        assert create_report_mock.called == 1
        assert is_inhibitor(create_report_mock.reports[0])
    else:
        assert create_report_mock.called == 0


@pytest.mark.parametrize('is_persistently_mounted', (False, True))
def test_inhibition_when_iso_not_on_persistent_partition(monkeypatch, is_persistently_mounted):
    path_mountpoint = '/d0/d1'
    iso_path = '/d0/d1/d2/d3/iso'
    create_report_mock = create_report_mocked()
    monkeypatch.setattr(reporting, 'create_report', create_report_mock)

    def os_path_ismount_mocked(path):
        if path == path_mountpoint:
            return True
        if path == '/':  # / Should be a mountpoint on every system
            return True
        return False

    monkeypatch.setattr(os.path, 'ismount', os_path_ismount_mocked)

    fstab_mountpoint = path_mountpoint if is_persistently_mounted else '/some/other/mountpoint'
    fstab_entry = FstabEntry(fs_spec='/dev/sta2', fs_file=fstab_mountpoint,
                             fs_vfstype='', fs_mntops='', fs_freq='', fs_passno='')
    storage_info_msg = StorageInfo(fstab=[fstab_entry])

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[storage_info_msg]))

    target_iso_msg = TargetOSInstallationImage(path=iso_path, mountpoint='', repositories=[])
    check_target_iso.inhibit_if_iso_not_located_on_persistent_partition(target_iso_msg)

    if is_persistently_mounted:
        assert not create_report_mock.called
    else:
        assert create_report_mock.called == 1
        assert is_inhibitor(create_report_mock.reports[0])


def test_actor_does_not_perform_when_iso_not_used(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())

    check_target_iso.perform_target_iso_checks()

    assert not reporting.create_report.called


# repos/system_upgrade/common/actors/checktargetrepos/libraries/checktargetrepos.py

from leapp import reporting
from leapp.libraries.common import config, rhsm
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepositoryFile, RHUIInfo, TargetRepositories

# TODO: we need to provide this path in a shared library
CUSTOM_REPO_PATH = '/etc/leapp/files/leapp_upgrade_repositories.repo'


def _any_custom_repo_defined():
    for tr in api.consume(TargetRepositories):
        if tr.custom_repos:
            return True
    return False


def _the_custom_repofile_defined():
    for ctrf in api.consume(CustomTargetRepositoryFile):
        if ctrf and ctrf.file == CUSTOM_REPO_PATH:
            return True
    return False


def _the_enablerepo_option_used():
    return config.get_env('LEAPP_ENABLE_REPOS', None) is not None


def process():
    target_major_version = get_target_major_version()

    if target_major_version == '8':
        ipu_doc_url = (
            'https://access.redhat.com/documentation/en-us/'
            'red_hat_enterprise_linux/8/html-single/upgrading_to_rhel_8/index'
        )
    elif target_major_version == '9':
        ipu_doc_url = ('TBA')

    rhui_info = next(api.consume(RHUIInfo), None)

    if not rhsm.skip_rhsm() or rhui_info:
        # getting RH repositories through RHSM or RHUI; resolved by seatbelts
        # implemented in other actors
        return

    # rhsm skipped; take your seatbelts please
    is_ctr = _any_custom_repo_defined()
    is_ctrf = _the_custom_repofile_defined()
    is_re = _the_enablerepo_option_used()
    if not is_ctr:
        # no rhsm, no custom repositories.. this will really not work :)
        # TODO: add link to the RH article about use of custom repositories!!
        # NOTE: we can put here now the link to the main document, as this
        # will be described there or at least the link to the right document
        # will be delivered here.
        if is_ctrf:
            summary_ctrf = '\n\nThe custom repository file has been detected. Maybe it is empty?'
        else:
            summary_ctrf = ''
        reporting.create_report([
            reporting.Title('Using RHSM has been skipped but no custom or RHUI repositories have been delivered.'),
            reporting.Summary(
                'Leapp is run in the mode when the Red Hat Subscription Manager'
                ' is not used (the --no-rhsm option or the LEAPP_NO_RHSM=1'
                ' environment variable has been set) so leapp is not able to'
                ' obtain YUM/DNF repositories with the content for the target'
                ' system in the standard way. The content has to be delivered'
                ' either by user manually or, in case of public clouds, by a'
                ' special Leapp package for RHUI environments.'
                ),
            reporting.Remediation(hint=(
                'Create the repository file according to instructions in the'
                ' referred document on the following path with all'
                ' repositories that should be used during the upgrade: "{}".'
                '\n\n{}'
                .format(CUSTOM_REPO_PATH, summary_ctrf)
            )),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.SANITY]),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.ExternalLink(url=ipu_doc_url, title='UPGRADING TO RHEL {}'.format(target_major_version)),
            reporting.RelatedResource('file', CUSTOM_REPO_PATH),
        ])
    elif not (is_ctrf or is_re):
        # Some custom repositories have been discovered, but the custom repo
        # file not - neither the --enablerepo option is used. Inform about
        # the official recommended way.
        reporting.create_report([
            reporting.Title('Detected "CustomTargetRepositories" without using new provided mechanisms used.'),
            reporting.Summary(
                'Red Hat now provides an official way for using custom'
                ' repositories during the in-place upgrade through'
                ' the referred custom repository file or through the'
                ' --enablerepo option for leapp. The CustomTargetRepositories'
                ' have been produced from custom (own) actors?'
            ),
            reporting.Remediation(hint=(
                'Follow the new simple way to enable custom repositories'
                ' during the upgrade (see the referred document) or create'
                ' the empty custom repository file to acknowledge this report'
                ' message.'
            )),
            reporting.Severity(reporting.Severity.INFO),
            reporting.ExternalLink(url=ipu_doc_url, title='UPGRADING TO RHEL {}'.format(target_major_version)),
            reporting.RelatedResource('file', CUSTOM_REPO_PATH),
        ])


# repos/system_upgrade/common/actors/checktargetrepos/tests/test_checktargetrepos.py

import pytest

from leapp import reporting
from leapp.libraries.actor import checktargetrepos
from leapp.libraries.common import rhsm
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import (
    CustomTargetRepository,
    CustomTargetRepositoryFile,
    EnvVar,
    Report,
    RepositoryData,
    RHELTargetRepository,
    TargetRepositories
)
from leapp.utils.report import is_inhibitor


class MockedConsume(object):
    def __init__(self, *args):
        self._msgs = []
        for arg in args:
            if not arg:
                continue
            if isinstance(arg, list):
                self._msgs.extend(arg)
            else:
                self._msgs.append(arg)

    def __call__(self, model):
        return iter([msg for msg in self._msgs if isinstance(msg, model)])


_RHEL_REPOS = [
    RHELTargetRepository(repoid='repo1'),
    RHELTargetRepository(repoid='repo2'),
    RHELTargetRepository(repoid='repo3'),
    RHELTargetRepository(repoid='repo4'),
]

_CUSTOM_REPOS = [
    CustomTargetRepository(repoid='repo1', name='repo1name', baseurl='repo1url', enabled=True),
    CustomTargetRepository(repoid='repo2', name='repo2name', baseurl='repo2url', enabled=False),
    CustomTargetRepository(repoid='repo3', name='repo3name', baseurl=None, enabled=True),
    CustomTargetRepository(repoid='repo4', name='repo4name', baseurl=None, enabled=True),
]

_TARGET_REPOS_CUSTOM = TargetRepositories(rhel_repos=_RHEL_REPOS, custom_repos=_CUSTOM_REPOS)
_TARGET_REPOS_NO_CUSTOM = TargetRepositories(rhel_repos=_RHEL_REPOS)
_CUSTOM_TARGET_REPOFILE = CustomTargetRepositoryFile(file='/etc/leapp/files/leapp_upgrade_repositories.repo')


def test_checktargetrepos_rhsm(monkeypatch):
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    monkeypatch.setattr(api, 'consume', MockedConsume())
    monkeypatch.setattr(checktargetrepos, 'get_target_major_version', lambda: '8')
    checktargetrepos.process()
    assert reporting.create_report.called == 0


@pytest.mark.parametrize('enable_repos', [True, False])
@pytest.mark.parametrize('custom_target_repos', [True, False])
@pytest.mark.parametrize('custom_target_repofile', [True, False])
def test_checktargetrepos_no_rhsm(monkeypatch, enable_repos, custom_target_repos, custom_target_repofile):
    mocked_consume = MockedConsume(_TARGET_REPOS_CUSTOM if custom_target_repos else _TARGET_REPOS_NO_CUSTOM)
    if custom_target_repofile:
        mocked_consume._msgs.append(_CUSTOM_TARGET_REPOFILE)
    envars = {'LEAPP_ENABLE_REPOS': 'hill,spencer'} if enable_repos else {}
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(envars=envars))

    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: True)
    monkeypatch.setattr(api, 'consume', mocked_consume)
    monkeypatch.setattr(checktargetrepos, 'get_target_major_version', lambda: '8')

    checktargetrepos.process()

    if not custom_target_repos:
        assert reporting.create_report.called == 1
        assert is_inhibitor(reporting.create_report.report_fields)
    elif not enable_repos and custom_target_repos and not custom_target_repofile:
        assert reporting.create_report.called == 1
        assert not is_inhibitor(reporting.create_report.report_fields)
    else:
        assert reporting.create_report.called == 0


# repos/system_upgrade/common/actors/checkyumpluginsenabled/libraries/checkyumpluginsenabled.py

import os

from leapp import reporting
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.common.rhsm import skip_rhsm

# If LEAPP_NO_RHSM is set, subscription-manager and product-id will not be
# considered as required when checking whether the required plugins are enabled.
REQUIRED_YUM_PLUGINS = {'subscription-manager', 'product-id'}
FMT_LIST_SEPARATOR = '\n    - '


def check_required_yum_plugins_enabled(pkg_manager_info):
    """
    Checks whether the yum plugins required by the IPU are enabled.

    If they are not enabled, a report is produced informing the user about it.

    :param pkg_manager_info: PkgManagerInfo
    """

    missing_required_plugins = REQUIRED_YUM_PLUGINS - set(pkg_manager_info.enabled_plugins)

    if skip_rhsm():
        missing_required_plugins -= {'subscription-manager', 'product-id'}

    if missing_required_plugins:
        missing_required_plugins_text = ''
        for missing_plugin in missing_required_plugins:
            missing_required_plugins_text += '{0}{1}'.format(FMT_LIST_SEPARATOR, missing_plugin)

        if get_source_major_version() == '7':
            pkg_manager = 'YUM'
            pkg_manager_config_path = '/etc/yum.conf'
            plugin_configs_dir = '/etc/yum/pluginconf.d'
        else:
            # On RHEL8+ the yum package might not be installed
            pkg_manager = 'DNF'
            pkg_manager_config_path = '/etc/dnf/dnf.conf'
            plugin_configs_dir = '/etc/dnf/plugins'

        # pkg_manager_config_path - enable/disable plugins globally
        # subscription_manager_plugin_conf, product_id_plugin_conf - plugins can be disabled individually
        subscription_manager_plugin_conf = os.path.join(plugin_configs_dir, 'subscription-manager.conf')
        product_id_plugin_conf = os.path.join(plugin_configs_dir, 'product-id.conf')

        remediation_commands = [
            'sed -i \'s/^plugins=0/plugins=1/\' \'{0}\''.format(pkg_manager_config_path),
            'sed -i \'s/^enabled=0/enabled=1/\' \'{0}\''.format(subscription_manager_plugin_conf),
            'sed -i \'s/^enabled=0/enabled=1/\' \'{0}\''.format(product_id_plugin_conf)
        ]

        reporting.create_report([
            reporting.Title('Required {0} plugins are not being loaded.'.format(pkg_manager)),
            reporting.Summary(
                'The following {0} plugins are not being loaded: {1}'.format(pkg_manager,
                                                                             missing_required_plugins_text)
            ),
            reporting.Remediation(
                hint='If you have yum plugins globally disabled, please enable them by editing the {0}. '
                     'Individually, the {1} plugins can be enabled in their corresponding configurations found at: {2}'
                     .format(pkg_manager_config_path, pkg_manager, plugin_configs_dir),
                # Provide all commands as one due to problems with satellites
                commands=[['bash', '-c', '"{0}"'.format('; '.join(remediation_commands))]]
            ),
            reporting.RelatedResource('file', pkg_manager_config_path),
            reporting.RelatedResource('file', subscription_manager_plugin_conf),
            reporting.RelatedResource('file', product_id_plugin_conf),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.REPOSITORY]),
        ])


# repos/system_upgrade/common/actors/checkyumpluginsenabled/tests/test_checkyumpluginsenabled.py

import pytest

from leapp import reporting
from leapp.libraries.actor.checkyumpluginsenabled import check_required_yum_plugins_enabled
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import PkgManagerInfo
from leapp.utils.report import is_inhibitor


def _create_report_with_multiple_base_list_primitive_fields():
    report_fields = [
            reporting.Title('a title'),
            reporting.Summary('some summary'),
            reporting.RelatedResource('file', '/path/to/some/configA'),
            reporting.RelatedResource('file', '/path/to/some/configB'),
            reporting.RelatedResource('file', '/path/to/some/configC'),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.REPOSITORY])]
    reporting.create_report(report_fields)


def test__create_report_mocked(monkeypatch):
    actor_reports = create_report_mocked()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(reporting, 'create_report', actor_reports)
    _create_report_with_multiple_base_list_primitive_fields()

    # make sure that all related resources are present
    for config in ['/path/to/some/configA', '/path/to/some/configB', '/path/to/some/configC']:
        assert config in [rr['title'] for rr in actor_reports.report_fields['detail']['related_resources']]

    # make sure that tags/flags joined as groups are present under groups
    for group in [reporting.Groups.INHIBITOR, reporting.Groups.REPOSITORY]:
        assert group in actor_reports.report_fields['groups']


def test_report_when_missing_required_plugins(monkeypatch):
    """Test whether a report entry is created when any of the required YUM plugins are missing."""
    yum_config = PkgManagerInfo(enabled_plugins=['product-id', 'some-user-plugin'])

    actor_reports = create_report_mocked()

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(reporting, 'create_report', actor_reports)

    check_required_yum_plugins_enabled(yum_config)

    assert actor_reports.called, "Report wasn't created when required a plugin is missing."

    fail_description = 'The missing required plugin is not mentioned in the report.'
    assert 'subscription-manager' in actor_reports.report_fields['summary'], fail_description

    fail_description = 'The upgrade should be inhibited when plugins are not enabled.'
    assert is_inhibitor(actor_reports.report_fields), fail_description


def test_nothing_is_reported_when_rhsm_disabled(monkeypatch):
    actor_mocked = CurrentActorMocked(envars={'LEAPP_NO_RHSM': '1'})

    monkeypatch.setattr(api, 'current_actor', actor_mocked)
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    yum_config = PkgManagerInfo(enabled_plugins=[])
    check_required_yum_plugins_enabled(yum_config)

    assert not reporting.create_report.called, 'Report was created even if LEAPP_NO_RHSM was set'


# repos/system_upgrade/common/actors/cloud/checkhybridimage/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkhybridimage import check_hybrid_image
from leapp.models import FirmwareFacts, HybridImage, InstalledRPM
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckHybridImage(Actor):
    """
    Check if the system is using Azure hybrid image.

    These images have a default relative symlink to EFI
    partition even when booted using BIOS and in such cases
    GRUB is not able find "grubenv" to get the kernel cmdline
    options and fails to boot after upgrade`.
    """

    name = 'checkhybridimage'
    consumes = (InstalledRPM, FirmwareFacts)
    produces = (HybridImage,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        check_hybrid_image()


# repos/system_upgrade/common/actors/cloud/checkrhui/actor.py

import os

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.common import rhsm, rhui
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.common.rpms import has_package
from leapp.libraries.stdlib import api
from leapp.models import (
    CopyFile,
    DNFPluginTask,
    InstalledRPM,
    KernelCmdlineArg,
    RequiredTargetUserspacePackages,
    RHUIInfo,
    RpmTransactionTasks,
    TargetUserSpacePreupgradeTasks
)
from leapp.reporting import create_report, Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class CheckRHUI(Actor):
    """
    Check if system is using RHUI infrastructure (on public cloud) and send messages to
    provide additional data needed for upgrade.
    """

    name = 'checkrhui'
    consumes = (InstalledRPM)
    produces = (
        KernelCmdlineArg,
        RHUIInfo,
        RequiredTargetUserspacePackages,
        Report, DNFPluginTask,
        RpmTransactionTasks,
        TargetUserSpacePreupgradeTasks,
        CopyFile,
    )
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        upg_path = rhui.get_upg_path()
        for provider, info in rhui.RHUI_CLOUD_MAP[upg_path].items():
            if has_package(InstalledRPM, info['src_pkg']):
                # we need to do this workaround in order to overcome our RHUI handling limitation
                # in case there are more client packages on the source system
                if 'azure' in info['src_pkg']:
                    azure_sap_variants = [
                        'azure-sap',
                        'azure-sap-apps',
                    ]
                    for azure_sap_variant in azure_sap_variants:
                        sap_variant_info = rhui.RHUI_CLOUD_MAP[upg_path][azure_sap_variant]
                        if has_package(InstalledRPM, sap_variant_info['src_pkg']):
                            info = sap_variant_info
                            provider = azure_sap_variant

                if provider.startswith('google'):
                    rhui_dir = api.get_common_folder_path('rhui')
                    repofile = os.path.join(rhui_dir, provider, 'leapp-{}.repo'.format(provider))
                    api.produce(
                        TargetUserSpacePreupgradeTasks(
                            copy_files=[CopyFile(src=repofile, dst='/etc/yum.repos.d/leapp-google-copied.repo')]
                        )
                    )

                if not rhsm.skip_rhsm():
                    create_report([
                        reporting.Title('Upgrade initiated with RHSM on public cloud with RHUI infrastructure'),
                        reporting.Summary(
                            'Leapp detected this system is on public cloud with RHUI infrastructure '
                            'but the process was initiated without "--no-rhsm" command line option '
                            'which implies RHSM usage (valid subscription is needed).'
                        ),
                        reporting.Severity(reporting.Severity.INFO),
                        reporting.Groups([reporting.Groups.PUBLIC_CLOUD]),
                    ])
                    return

                # When upgrading with RHUI we cannot switch certs and let RHSM provide us repos for target OS content.
                # Instead, Leapp's provider-specific package containing target OS certs and repos has to be installed.
                if not has_package(InstalledRPM, info['leapp_pkg']):
                    create_report([
                        reporting.Title('Package "{}" is missing'.format(info['leapp_pkg'])),
                        reporting.Summary(
                            'On {} using RHUI infrastructure, a package "{}" is needed for'
                            'in-place upgrade'.format(provider.upper(), info['leapp_pkg'])
                        ),
                        reporting.Severity(reporting.Severity.HIGH),
                        reporting.RelatedResource('package', info['leapp_pkg']),
                        reporting.Groups([reporting.Groups.INHIBITOR]),
                        reporting.Groups([reporting.Groups.PUBLIC_CLOUD, reporting.Groups.RHUI]),
                        reporting.Remediation(commands=[['yum', 'install', '-y', info['leapp_pkg']]])
                    ])
                    return

                # there are several "variants" related to the *AWS* provider (aws, aws-sap)
                if provider.startswith('aws'):
                    # We have to disable Amazon-id plugin in the initramdisk phase as the network
                    # is down at the time
                    self.produce(DNFPluginTask(name='amazon-id', disable_in=['upgrade']))

                # If source OS and target OS packages differ we must remove the source pkg, and install the target pkg.
                # If the packages do not differ, it is sufficient to upgrade them during the upgrade
                if info['src_pkg'] != info['target_pkg']:
                    self.produce(RpmTransactionTasks(to_install=[info['target_pkg']]))
                    self.produce(RpmTransactionTasks(to_remove=[info['src_pkg']]))
                    # Handle azure SAP systems that use two RHUI clients - one for RHEL content, one for SAP content
                    if provider == 'azure-sap':
                        azure_nonsap_pkg = rhui.RHUI_CLOUD_MAP[upg_path]['azure']['src_pkg']
                        self.produce(RpmTransactionTasks(to_remove=[azure_nonsap_pkg]))
                    elif provider == 'azure-sap-apps':
                        # SAP Apps systems have EUS content channel from RHEL8+
                        src_rhel_content_type = 'azure' if get_source_major_version() == '7' else 'azure-eus'
                        azure_nonsap_pkg = rhui.RHUI_CLOUD_MAP[upg_path][src_rhel_content_type]['src_pkg']
                        self.produce(RpmTransactionTasks(to_remove=[azure_nonsap_pkg]))

                self.produce(RHUIInfo(provider=provider))
                self.produce(RequiredTargetUserspacePackages(packages=[info['target_pkg']]))
                return


# repos/system_upgrade/common/actors/cloud/grubenvtofile/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.grubenvtofile import grubenv_to_file
from leapp.models import HybridImage
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class GrubenvToFile(Actor):
    """
    Convert "grubenv" symlink to a regular file on Azure hybrid images using BIOS.

    Azure images provided by Red Hat aim for hybrid (BIOS/EFI) functionality,
    however, currently GRUB is not able to see the "grubenv" file if it is a symlink
    to a different partition (default on EFI with grub2-efi pkg installed) and
    fails on BIOS systems. This actor converts the symlink to the normal file
    with the content of grubenv on the EFI partition in case the system is using BIOS
    and running on the Azure cloud. This action is reported in the preupgrade phase.
    """

    name = 'grubenvtofile'
    consumes = (HybridImage,)
    produces = ()
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        grubenv_msg = next(self.consume(HybridImage), None)

        if grubenv_msg and grubenv_msg.detected:
            grubenv_to_file()


# repos/system_upgrade/common/actors/commonleappdracutmodules/libraries/modscan.py

import os
import re

from leapp.libraries.common.config import architecture, version
from leapp.libraries.stdlib import api
from leapp.utils.deprecation import suppress_deprecation

from leapp.models import (  # isort:skip
    RequiredUpgradeInitramPackages,  # deprecated
    UpgradeDracutModule,  # deprecated
    DracutModule,
    TargetUserSpaceUpgradeTasks,
    UpgradeInitramfsTasks
)

_REQUIRED_PACKAGES = [
    'binutils',
    'cifs-utils',
    'device-mapper-multipath',
    'dracut',
    'dracut-config-generic',
    'dracut-config-rescue',
    'dracut-network',
    'dracut-tools',
    'fcoe-utils',
    'hostname',
    'iscsi-initiator-utils',
    'kbd',
    'kernel',
    'kernel-core',
    'kernel-modules',
    'keyutils',
    'lldpad',
    'lvm2',
    'mdadm',
    'nfs-utils',
    'openssh-clients',
    'plymouth',
    'rpcbind',
    'systemd-container',
    'tar'
]


# The decorator is not effective for generators, it has to be used one level
# above
# @suppress_deprecation(UpgradeDracutModule)
def _create_dracut_modules():
    dracut_base_path = api.get_actor_folder_path('dracut')
    if dracut_base_path:
        dracut_base_path = os.path.abspath(dracut_base_path)
        for module in os.listdir(dracut_base_path):
            yield UpgradeDracutModule(
                name=re.sub(r'^\d+', '', module),
                module_path=os.path.join(dracut_base_path, module)
            )
            # NOTE: when the UpgradeDracutModule is dropped, this could be
            # handled just by one msg instead of two
            dm = DracutModule(
                name=re.sub(r'^\d+', '', module),
                module_path=os.path.join(dracut_base_path, module))
            yield UpgradeInitramfsTasks(include_dracut_modules=[dm])


@suppress_deprecation(RequiredUpgradeInitramPackages)
def _create_initram_packages():
    # copy the list as we do not want to affect the constant because of tests
    required_pkgs = _REQUIRED_PACKAGES[:]
    if architecture.matches_architecture(architecture.ARCH_X86_64):
        required_pkgs.append('biosdevname')
    if version.get_target_major_version() == '9':
        required_pkgs += ['policycoreutils', 'rng-tools']
    return (
        RequiredUpgradeInitramPackages(packages=required_pkgs),
        TargetUserSpaceUpgradeTasks(install_rpms=required_pkgs)
    )


def process():
    api.produce(*tuple(_create_dracut_modules()))
    api.produce(*_create_initram_packages())


# repos/system_upgrade/common/actors/commonleappdracutmodules/tests/test_modscan_commonleappdracutmodules.py

import os.path
import re
from collections import namedtuple

import pytest

from leapp.libraries.actor import modscan
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.utils.deprecation import suppress_deprecation

from leapp.models import (  # isort:skip
    RequiredUpgradeInitramPackages,  # deprecated
    UpgradeDracutModule,  # deprecated
    TargetUserSpaceUpgradeTasks,
    UpgradeInitramfsTasks
)


def _files_get_folder_path(name):
    path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'files', name)
    assert os.path.isdir(path)
    return path


@suppress_deprecation(UpgradeDracutModule)
def test_created_modules(monkeypatch):
    monkeypatch.setattr(api, 'get_actor_folder_path', _files_get_folder_path)
    path = os.path.abspath(api.get_actor_folder_path('dracut'))
    required_modules = {'sys-upgrade', 'sys-upgrade-redhat'}
    # _old is for deprecated stuff, _new is ...
    # we want to test that the new and original solution produce 'equivalent' output
    produced_required_modules_old = set()
    produced_required_modules_new = set()

    def check_module(mod):
        # Ensures that this actor only includes known modules
        assert mod.name in required_modules

        # Ensures that the path is valid
        assert mod.module_path
        assert mod.module_path == os.path.abspath(mod.module_path)

        # Ensure it's a directory
        assert os.path.isdir(mod.module_path)

        # Ensure it's located within the actors files path
        assert mod.module_path.startswith(path)

        # Ensure the directory name ends with the module name
        assert os.path.basename(mod.module_path).endswith(mod.name)
        assert not re.sub(r'^(85|90){}$'.format(mod.name), '', os.path.basename(mod.module_path))

    for msg in modscan._create_dracut_modules():
        if isinstance(msg, UpgradeDracutModule):
            produced_required_modules_old.add(msg.name)
            check_module(msg)
        else:
            for mod in msg.include_dracut_modules:
                produced_required_modules_new.add(mod.name)
                check_module(mod)

    # ensure that old and new solution produce equivalent (expected) output
    assert produced_required_modules_new == produced_required_modules_old == required_modules


@pytest.mark.parametrize('src_ver,dst_ver,arch', (
    ('7.9', '8.4', architecture.ARCH_X86_64),
    ('7.9', '8.4', architecture.ARCH_S390X),
    ('8.4', '9.0', architecture.ARCH_X86_64),
    ('8.4', '9.0', architecture.ARCH_S390X),
))
def test_required_packages(monkeypatch, src_ver, dst_ver, arch):
    # the default set of required rpms should not contain biosdevname
    for pkg in ['biosdevname', 'policycoreutils', 'rng-tools']:
        assert pkg not in modscan._REQUIRED_PACKAGES

    required_packages = modscan._REQUIRED_PACKAGES[:]
    if dst_ver[0] == '9':
        required_packages += ['policycoreutils', 'rng-tools']
    if arch == architecture.ARCH_X86_64:
        required_packages += ['biosdevname']

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver=src_ver, dst_ver=dst_ver, arch=arch))
    old_initram, new_initram = modscan._create_initram_packages()
    assert (set(required_packages)
            == set(old_initram.packages)
            == set(new_initram.install_rpms))


@suppress_deprecation(UpgradeDracutModule)
def test_process_produces_modules(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    messages = []
    monkeypatch.setattr(api, 'produce', lambda *x: messages.extend(x))
    monkeypatch.setattr(api, 'get_actor_folder_path', _files_get_folder_path)
    modscan.process()
    assert messages
    assert len(messages) == 6
    assert len([msg for msg in messages if isinstance(msg, UpgradeDracutModule)]) == 2
    assert len([msg for msg in messages if isinstance(msg, UpgradeInitramfsTasks)]) == 2
    assert len([msg for msg in messages if isinstance(msg, RequiredUpgradeInitramPackages)]) == 1
    assert len([msg for msg in messages if isinstance(msg, TargetUserSpaceUpgradeTasks)]) == 1


# repos/system_upgrade/common/actors/createisorepofile/libraries/create_iso_repofile.py

import os

from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepositoryFile, TargetOSInstallationImage


def produce_repofile_if_iso_used():
    target_iso_msgs_iter = api.consume(TargetOSInstallationImage)
    target_iso = next(target_iso_msgs_iter, None)

    if not target_iso:
        return

    if next(target_iso_msgs_iter, None):
        api.current_logger().warn('Received multiple TargetISInstallationImage messages, using the first one')

    # Mounting was successful, create a repofile to copy into target userspace
    repofile_entry_template = ('[{repoid}]\n'
                               'name={reponame}\n'
                               'baseurl={baseurl}\n'
                               'enabled=0\n'
                               'gpgcheck=0\n')

    repofile_content = ''
    for repo in target_iso.repositories:
        repofile_content += repofile_entry_template.format(repoid=repo.repoid,
                                                           reponame=repo.repoid,
                                                           baseurl=repo.baseurl)

    target_os_path_prefix = 'el{target_major_ver}'.format(target_major_ver=get_target_major_version())
    iso_repofile_path = os.path.join('/var/lib/leapp/', '{}_iso.repo'.format(target_os_path_prefix))
    with open(iso_repofile_path, 'w') as iso_repofile:
        iso_repofile.write(repofile_content)

    api.produce(CustomTargetRepositoryFile(file=iso_repofile_path))


# repos/system_upgrade/common/actors/createresumeservice/tests/test_createresumeservice.py

import os

import distro
import pytest


@pytest.mark.skipif(os.getuid() != 0, reason='User is not a root')
@pytest.mark.skipif(
    distro.linux_distribution()[0] == 'Fedora',
    reason='default.target.wants does not exists on Fedora distro',
)
def test_create_resume_service(current_actor_context):

    current_actor_context.run()

    service_name = 'leapp_resume.service'
    service_path = '/etc/systemd/system/{}'.format(service_name)
    symlink_path = '/etc/systemd/system/default.target.wants/{}'.format(service_name)

    try:
        assert os.path.isfile(service_path)
        assert os.path.isfile(symlink_path)
    finally:
        os.unlink(service_path)
        os.unlink(symlink_path)


# repos/system_upgrade/common/actors/detectkerneldrivers/libraries/detectkerneldrivers.py

from leapp.libraries.stdlib import api
from leapp.models import ActiveKernelModulesFacts, DetectedDeviceOrDriver, DeviceDriverDeprecationData


def process():
    loaded_drivers = {
        module.filename
        for message in api.consume(ActiveKernelModulesFacts)
        for module in message.kernel_modules
    }
    driver_data = {
        entry.driver_name: entry
        for message in api.consume(DeviceDriverDeprecationData)
        for entry in message.entries
        if not entry.device_id
    }
    api.produce(*[
        DetectedDeviceOrDriver(**driver_data[driver].dump())
        for driver in loaded_drivers
        if driver in driver_data
    ])


# repos/system_upgrade/common/actors/detectmissingnewlineingrubcfg/libraries/detectmissingnewlineingrubcfg.py

import os


def _get_config_contents(config_path):
    if os.path.isfile(config_path):
        with open(config_path, 'r') as config:
            return config.read()
    return ''


def is_grub_config_missing_final_newline(conf_file):
    config_contents = _get_config_contents(conf_file)
    return config_contents != '' and config_contents[-1] != '\n'


# repos/system_upgrade/common/actors/detectmissingnewlineingrubcfg/tests/test_detectmissingnewlineingrubcfg.py

import pytest

from leapp.libraries.actor import detectmissingnewlineingrubcfg


@pytest.mark.parametrize(
    ('config_contents', 'error_detected'),
    [
        ('GRUB_DEFAULT=saved\nGRUB_DISABLE_SUBMENU=true\n', False),
        ('GRUB_DEFAULT=saved\nGRUB_DISABLE_SUBMENU=true', True)
    ]
)
def test_is_grub_config_missing_final_newline(monkeypatch, config_contents, error_detected):

    config_path = '/etc/default/grub'

    def mocked_get_config_contents(path):
        assert path == config_path
        return config_contents

    monkeypatch.setattr(detectmissingnewlineingrubcfg, '_get_config_contents', mocked_get_config_contents)

    assert detectmissingnewlineingrubcfg.is_grub_config_missing_final_newline(config_path) == error_detected


# repos/system_upgrade/common/actors/efibootorderfix/eficheckboot/actor.py

import os

from leapp import reporting
from leapp.actors import Actor
from leapp.models import FirmwareFacts
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class EfiCheckBoot(Actor):
    """
    Adjust EFI boot entry for first reboot
    """

    name = 'efi_check_boot'
    consumes = (FirmwareFacts,)
    produces = (reporting.Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        is_system_efi = False
        has_efibootmgr = os.path.exists('/sbin/efibootmgr')
        for fact in self.consume(FirmwareFacts):
            if fact.firmware == 'efi':
                is_system_efi = True
                break

        if is_system_efi and not has_efibootmgr:
            reporting.create_report([
                reporting.Title('efibootmgr package is required on EFI systems'),
                reporting.Summary(
                    'efibootmgr is required so that we can can set proper boot options in between restarts'
                ),
                reporting.Remediation(commands=[['yum', '-y', 'install', 'efibootmgr']]),
                reporting.RelatedResource('package', 'efibootmgr'),
                reporting.Groups([reporting.Groups.INHIBITOR]),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups([reporting.Groups.BOOT])
            ])


# repos/system_upgrade/common/actors/efibootorderfix/finalization/actor.py

from leapp.actors import Actor
from leapp.libraries.common import efi_reboot_fix
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class EfiFinalizationFix(Actor):
    """
    Adjust EFI boot entry for final reboot
    """

    name = 'efi_finalization_fix'
    consumes = ()
    produces = ()
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        efi_reboot_fix.maybe_emit_updated_boot_entry()


# repos/system_upgrade/common/actors/efibootorderfix/interim/actor.py

from leapp.actors import Actor
from leapp.libraries.common import efi_reboot_fix
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag


class EfiInterimFix(Actor):
    """
    Adjust EFI boot entry for first reboot
    """

    name = 'efi_interim_fix'
    consumes = ()
    produces = ()
    tags = (InterimPreparationPhaseTag, IPUWorkflowTag)

    def process(self):
        efi_reboot_fix.maybe_emit_updated_boot_entry()


# repos/system_upgrade/common/actors/enablerhsmtargetrepos/libraries/enablerhsmtargetrepos.py

from leapp.libraries.common import config, mounting, rhsm
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import UsedTargetRepositories


def set_rhsm_release():
    """Set the RHSM release to the target RHEL minor version."""
    if rhsm.skip_rhsm():
        api.current_logger().debug('Skipping setting the RHSM release due to --no-rhsm or environment variables.')
        return

    if config.get_product_type('target') != 'ga':
        api.current_logger().debug('Skipping setting the RHSM release as target product is set to beta/htb')
        return
    target_version = api.current_actor().configuration.version.target
    try:
        rhsm.set_release(mounting.NotIsolatedActions(base_dir='/'), target_version)
    except CalledProcessError as err:
        api.current_logger().warning('Unable to set the {0} release through subscription-manager. When using dnf,'
                                     ' content of the latest RHEL {1} minor version will be downloaded.\n{2}'
                                     .format(target_version, get_target_major_version(), str(err)))


def enable_rhsm_repos():
    """
    Try enabling all the target RHEL repositories that have been used for the upgrade transaction.

    In case of custom repositories, the subscription-manager reports an error that it doesn't know them, but it enables
    the known repositories.
    """
    if rhsm.skip_rhsm():
        api.current_logger().debug('Skipping enabling repositories through subscription-manager due to --no-rhsm'
                                   ' or environment variables.')
        return
    try:
        run(get_submgr_cmd(get_repos_to_enable()))
    except CalledProcessError as err:
        api.current_logger().warning('The subscription-manager could not enable some repositories.\n'
                                     'It is expected behavior in case of custom repositories unknown to'
                                     ' the subscription-manager - these need to be enabled manually.\n{0}'
                                     .format(str(err)))


def get_submgr_cmd(repos_to_enable):
    submgr_cmd = ['subscription-manager', 'repos']
    for repoid in repos_to_enable:
        submgr_cmd += ['--enable', repoid]
    return submgr_cmd


def get_repos_to_enable():
    """
    Return set of repositories used during the upgrade transaction.

    This set may include repos unknown to subscription-manager - notable those added as custom using
    CustomTargetRepository model.
    """
    used_repos_msg = next(api.consume(UsedTargetRepositories), None)
    return {repo.repoid for repo in used_repos_msg.repos}


# repos/system_upgrade/common/actors/enablerhsmtargetrepos/tests/test_enablerhsmtargetrepos.py

import sys

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import enablerhsmtargetrepos
from leapp.libraries.common import config, mounting, rhsm
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import EnvVar, UsedTargetRepositories, UsedTargetRepository


def not_isolated_actions(raise_err=False):
    commands_called = []

    class MockNotIsolatedActions(object):
        def __init__(self, base_dir=None):
            pass

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

    return (commands_called, MockNotIsolatedActions)


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


class run_mocked(object):
    def __init__(self, raise_err=False):
        self.called = 0
        self.args = []
        self.raise_err = raise_err

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


def test_setrelease(monkeypatch):
    commands_called, klass = not_isolated_actions()
    monkeypatch.setattr(mounting, 'NotIsolatedActions', klass)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='8.0'))
    monkeypatch.setattr(config, 'get_product_type', lambda dummy: 'ga')
    enablerhsmtargetrepos.set_rhsm_release()
    assert commands_called and len(commands_called) == 1
    assert commands_called[0][0][-1] == '8.0'


def test_setrelease_submgr_throwing_error(monkeypatch):
    _, klass = not_isolated_actions(raise_err=True)
    monkeypatch.setattr(mounting, 'NotIsolatedActions', klass)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='8.0', envars={'LEAPP_NO_RHSM': '0'}))
    monkeypatch.setattr(config, 'get_product_type', lambda dummy: 'ga')
    # free the set_release function from the @_rhsm_retry decorator
    # which would otherwise cause 25 sec delay of the test
    if sys.version_info.major < 3:
        monkeypatch.setattr(rhsm, 'set_release',
                            rhsm.set_release.func_closure[0].cell_contents.func_closure[0].cell_contents)
    else:
        monkeypatch.setattr(rhsm, 'set_release', rhsm.set_release.__wrapped__.__wrapped__)
    with pytest.raises(StopActorExecutionError):
        enablerhsmtargetrepos.set_rhsm_release()


@pytest.mark.parametrize('product', ['beta', 'htb'])
def test_setrelease_skip_rhsm(monkeypatch, product):
    commands_called, _ = not_isolated_actions()
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(envars={'LEAPP_NO_RHSM': '1'}))
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(config, 'get_product_type', lambda dummy: product)
    # To make this work we need to re-apply the decorator, so it respects the environment variable
    monkeypatch.setattr(rhsm, 'set_release', rhsm.with_rhsm(rhsm.set_release))
    enablerhsmtargetrepos.set_rhsm_release()
    assert not commands_called


def construct_UTRepo_consume(repoids):
    repos = [UsedTargetRepository(repoid=repoid) for repoid in repoids]
    return lambda *x: (x for x in (UsedTargetRepositories(repos=repos),))


def test_get_unique_repoids(monkeypatch):
    repoids = (['some-repo', 'some-repo', 'another-repo'])
    monkeypatch.setattr(api, 'consume', construct_UTRepo_consume(repoids))
    assert enablerhsmtargetrepos.get_repos_to_enable() == {'some-repo', 'another-repo'}


def test_get_submgr_cmd():
    assert enablerhsmtargetrepos.get_submgr_cmd({'some-repo'}) == ['subscription-manager', 'repos', '--enable',
                                                                   'some-repo']


def test_running_submgr_ok(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='8.0', envars={'LEAPP_NO_RHSM': '0'}), )
    monkeypatch.setattr(enablerhsmtargetrepos, 'get_repos_to_enable', lambda: {'some-repo'})
    monkeypatch.setattr(enablerhsmtargetrepos, 'run', run_mocked())
    enablerhsmtargetrepos.enable_rhsm_repos()
    assert enablerhsmtargetrepos.run.called
    assert 'subscription-manager' in enablerhsmtargetrepos.run.args[0][0]


def test_running_submgr_fail(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='8.0', envars={'LEAPP_NO_RHSM': '0'}), )
    monkeypatch.setattr(enablerhsmtargetrepos, 'get_repos_to_enable', lambda: {'some-repo'})
    monkeypatch.setattr(enablerhsmtargetrepos, 'run', run_mocked(raise_err=True))
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    enablerhsmtargetrepos.enable_rhsm_repos()
    assert enablerhsmtargetrepos.run.called
    assert api.current_logger.warnmsg


def test_enable_repos_skip_rhsm(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(envars={'LEAPP_NO_RHSM': '1'}))
    monkeypatch.setattr(enablerhsmtargetrepos, 'run', run_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    enablerhsmtargetrepos.enable_rhsm_repos()
    assert not enablerhsmtargetrepos.run.called
    assert api.current_logger.dbgmsg


# repos/system_upgrade/common/actors/filterrpmtransactionevents/tests/test_filterrpmtransactionevents.py

from leapp.models import FilteredRpmTransactionTasks, InstalledRedHatSignedRPM, Module, RPM, RpmTransactionTasks
from leapp.snactor.fixture import current_actor_context

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


def test_actor_execution(current_actor_context):
    current_actor_context.run()
    assert current_actor_context.consume(FilteredRpmTransactionTasks)


def test_actor_execution_with_sample_data(current_actor_context):
    installed_rpm = [
        RPM(name='sample01', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_PGP_SIG'),
        RPM(name='sample02', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_PGP_SIG')]
    modules_to_enable = [Module(name='enable', stream='1'), Module(name='enable', stream='2')]
    modules_to_reset = [Module(name='reset', stream='1'), Module(name='reset', stream='2')]
    current_actor_context.feed(InstalledRedHatSignedRPM(items=installed_rpm))
    current_actor_context.feed(RpmTransactionTasks(
        to_remove=[rpm.name for rpm in installed_rpm],
        to_keep=[installed_rpm[0].name],
        modules_to_enable=modules_to_enable,
        modules_to_reset=modules_to_reset,
    ))
    current_actor_context.feed(RpmTransactionTasks(
        modules_to_enable=modules_to_enable,
        modules_to_reset=modules_to_reset,
    ))
    current_actor_context.run()
    result = current_actor_context.consume(FilteredRpmTransactionTasks)
    assert len(result) == 1
    assert result[0].to_keep == [installed_rpm[0].name]
    assert result[0].to_remove == [installed_rpm[1].name]

    assert len(result[0].modules_to_enable) == 2
    assert all(m.name == 'enable' for m in result[0].modules_to_enable)
    assert '1' in {m.stream for m in result[0].modules_to_enable}
    assert '2' in {m.stream for m in result[0].modules_to_enable}

    assert len(result[0].modules_to_reset) == 2
    assert all(m.name == 'reset' for m in result[0].modules_to_reset)
    assert '1' in {m.stream for m in result[0].modules_to_reset}
    assert '2' in {m.stream for m in result[0].modules_to_reset}


# repos/system_upgrade/common/actors/forcedefaultboottotargetkernelversion/libraries/forcedefaultboot.py

import os
from collections import namedtuple

from leapp.libraries import stdlib
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api, config
from leapp.models import InstalledTargetKernelVersion

KernelInfo = namedtuple('KernelInfo', ('kernel_path', 'initrd_path'))


def get_kernel_info(message):
    kernel_name = 'vmlinuz-{}'.format(message.version)
    initrd_name = 'initramfs-{}.img'.format(message.version)
    kernel_path = os.path.join('/boot', kernel_name)
    initrd_path = os.path.join('/boot', initrd_name)

    target_version_bootable = True
    if not os.path.exists(kernel_path):
        target_version_bootable = False
        api.current_logger().warning('Mandatory kernel %s does not exist', kernel_path)
    if not os.path.exists(initrd_path):
        target_version_bootable = False
        api.current_logger().warning('Mandatory initrd %s does not exist', initrd_path)

    if target_version_bootable:
        return KernelInfo(kernel_path=kernel_path, initrd_path=initrd_path)

    api.current_logger().warning('Skipping check due to missing mandatory files')
    return None


def update_default_kernel(kernel_info):
    try:
        stdlib.run(['grubby', '--info', kernel_info.kernel_path])
    except stdlib.CalledProcessError:
        api.current_logger().error('Expected kernel %s to be installed at the boot loader but cannot be found.',
                                   kernel_info.kernel_path)
    except OSError:
        api.current_logger().error('Could not check for kernel existence in boot loader. Is grubby installed?')
    else:
        try:
            stdlib.run(['grubby', '--set-default', kernel_info.kernel_path])
            if architecture.matches_architecture(architecture.ARCH_S390X):
                # on s390x we need to call zipl explicitly because of issue in grubby,
                # otherwise the new boot entry will not be set as default
                # See https://bugzilla.redhat.com/show_bug.cgi?id=1764306
                stdlib.run(['/usr/sbin/zipl'])
        except (OSError, stdlib.CalledProcessError):
            api.current_logger().error('Failed to set default kernel to: %s', kernel_info.kernel_path, exc_info=True)


def process():
    if (config.is_debug and not
            architecture.matches_architecture(architecture.ARCH_S390X)):  # pylint: disable=using-constant-test
        try:
            # the following command prints output of grubenv for debugging purposes and is repeated after setting
            # default kernel so we can be sure we have the right saved entry
            #
            # saved_entry=63...
            # kernelopts=root=/dev/mapper...
            #
            #
            # boot_success and boot_indeterminate parameters are added later by one-shot systemd service
            stdlib.run(['grub2-editenv', 'list'])
        except stdlib.CalledProcessError:
            api.current_logger().error('Failed to execute "grub2-editenv list" command')
    message = next(api.consume(InstalledTargetKernelVersion), None)
    if not message:
        api.current_logger().warning(('Skipped - Forcing checking and setting default boot entry to target kernel'
                                      ' version due to missing message'))
        return

    try:
        current_default_kernel = stdlib.run(['grubby', '--default-kernel'])['stdout'].strip()
    except (OSError, stdlib.CalledProcessError):
        api.current_logger().warning('Failed to query grubby for default kernel', exc_info=True)
        return

    for type_ in ('index', 'title'):
        try:
            stdlib.run(['grubby', '--default-{}'.format(type_)])
        except (OSError, stdlib.CalledProcessError):
            api.current_logger().warning('Failed to query grubby for default {}'.format(type_), exc_info=True)
            return

    kernel_info = get_kernel_info(message)
    if not kernel_info:
        return

    if current_default_kernel != kernel_info.kernel_path:
        api.current_logger().warning(('Current default boot entry not target kernel version: Current default: %s.'
                                      'Forcing default kernel to %s'),
                                     current_default_kernel, kernel_info.kernel_path)
        update_default_kernel(kernel_info)
    if (config.is_debug and not
            architecture.matches_architecture(architecture.ARCH_S390X)):  # pylint: disable=using-constant-test
        try:
            stdlib.run(['grub2-editenv', 'list'])
        except stdlib.CalledProcessError:
            api.current_logger().error('Failed to execute "grub2-editenv list" command')


# repos/system_upgrade/common/actors/forcedefaultboottotargetkernelversion/tests/test_forcedefaultboot_forcedefaultboottotargetkernelversion.py

import os.path
from collections import namedtuple

import pytest

from leapp.libraries import stdlib
from leapp.libraries.actor import forcedefaultboot
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import InstalledTargetKernelVersion

Expected = namedtuple(
    'Expected', (
     'grubby_setdefault',
     'zipl_called'
    )
)

Case = namedtuple(
    'Case',
    ('initrd_exists',
     'kernel_exists',
     'entry_default',
     'entry_exists',
     'message_available',
     'arch_s390x'
     )
)

TARGET_KERNEL_VERSION = '1.2.3.4.el8.x86_64'
TARGET_KERNEL_TITLE = 'Red Hat Enterprise Linux ({}) 8.1 (Ootpa)'.format(TARGET_KERNEL_VERSION)
TARGET_KERNEL_PATH = '/boot/vmlinuz-{}'.format(TARGET_KERNEL_VERSION)
TARGET_INITRD_PATH = '/boot/initramfs-{}.img'.format(TARGET_KERNEL_VERSION)

OLD_KERNEL_VERSION = '0.1.2.3.el7.x86_64'
OLD_KERNEL_TITLE = 'Red Hat Enterprise Linux ({}) 7.6 (Maipo)'.format(OLD_KERNEL_VERSION)
OLD_KERNEL_PATH = '/boot/vmlinuz-{}'.format(OLD_KERNEL_VERSION)

CASES = (
    (Case(initrd_exists=True, kernel_exists=True, entry_default=True, entry_exists=True, message_available=True,
          arch_s390x=False),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=False, kernel_exists=True, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=False),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=False, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=False),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=False, kernel_exists=False, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=False),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=True, entry_default=False, entry_exists=True, message_available=False,
          arch_s390x=False),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=True, entry_default=False, entry_exists=False, message_available=False,
          arch_s390x=False),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=True, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=False),
     Expected(grubby_setdefault=True, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=True, entry_default=True, entry_exists=True, message_available=True,
          arch_s390x=True),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=False, kernel_exists=True, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=True),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=False, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=True),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=False, kernel_exists=False, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=True),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=True, entry_default=False, entry_exists=True, message_available=False,
          arch_s390x=True),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=True, entry_default=False, entry_exists=False, message_available=False,
          arch_s390x=True),
     Expected(grubby_setdefault=False, zipl_called=False)),
    (Case(initrd_exists=True, kernel_exists=True, entry_default=False, entry_exists=True, message_available=True,
          arch_s390x=True),
     Expected(grubby_setdefault=True, zipl_called=True))
)

_GRUBBY_INFO_TEMPLATE = '''index={entry_index}
kernel=/boot/vmlinuz-{kernel_version}
args="ro rd.lvm.lv=testing/root rd.lvm.lv=testing/swap rhgb quiet LANG=en_US.UTF-8"
root=/dev/mapper/testing-root
initrd=/boot/initramfs-{kernel_version}.img
'''


class MockedRun(object):
    def __init__(self, case):
        self.case = case
        self.called_setdefault = False
        self.called_zipl = False

    def __call__(self, cmd, *args, **kwargs):
        if cmd and cmd[0] == 'grubby':
            target = getattr(self, 'grubby_{}'.format(cmd[1].strip('--').replace('-', '_')), None)
            assert target and 'Unsupport grubby command called'  # pylint: disable=simplifiable-condition
            return target(cmd)  # pylint: disable=not-callable
        if cmd and cmd[0] == '/usr/sbin/zipl':
            self.called_zipl = True
        return None

    def grubby_info(self, cmd):
        assert len(cmd) == 3
        if not self.case.entry_exists:
            raise stdlib.CalledProcessError('A leapp command failed', cmd, {})

        return {
            'stdout': _GRUBBY_INFO_TEMPLATE.format(
                entry_index=0 if self.case.entry_default else 1,
                kernel_version=TARGET_KERNEL_VERSION)
            }

    def grubby_default_kernel(self, cmd):
        assert len(cmd) == 2
        if self.case.entry_default:
            return {'stdout': '{}\n'.format(TARGET_KERNEL_PATH)}
        return {'stdout': '{}\n'.format(OLD_KERNEL_PATH)}

    def grubby_default_index(self, cmd):
        assert len(cmd) == 2
        if self.case.entry_default:
            return {'stdout': '1\n'}
        return {'stdout': '0\n'}

    def grubby_default_title(self, cmd):
        assert len(cmd) == 2
        if self.case.entry_default:
            return {'stdout': '{}\n'.format(TARGET_KERNEL_TITLE)}
        return {'stdout': '{}\n'.format(OLD_KERNEL_TITLE)}

    def grubby_set_default(self, cmd):
        assert len(cmd) == 3
        assert cmd[2] == TARGET_KERNEL_PATH
        self.called_setdefault = True


def mocked_consume(case):
    def impl(*args):
        if case.message_available:
            return iter((InstalledTargetKernelVersion(version=TARGET_KERNEL_VERSION),))
        return iter(())
    return impl


def mocked_exists(case, orig_path_exists):
    def impl(path):
        if path == TARGET_KERNEL_PATH:
            return case.kernel_exists
        if path == TARGET_INITRD_PATH:
            return case.initrd_exists
        return orig_path_exists(path)
    return impl


@pytest.mark.parametrize('case_result', CASES)
def test_force_default_boot_target_scenario(case_result, monkeypatch):
    case, result = case_result
    arch = architecture.ARCH_S390X if case.arch_s390x else architecture.ARCH_X86_64
    mocked_run = MockedRun(case)
    monkeypatch.setattr(api, 'consume', mocked_consume(case))
    monkeypatch.setattr(stdlib, 'run', mocked_run)
    monkeypatch.setattr(os.path, 'exists', mocked_exists(case, os.path.exists))
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch=arch))
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    forcedefaultboot.process()
    assert result.grubby_setdefault == mocked_run.called_setdefault
    assert result.zipl_called == mocked_run.called_zipl


# repos/system_upgrade/common/actors/getinstalleddesktops/libraries/getinstalleddesktops.py

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


def get_installed_desktops():
    api.current_logger().info("  Detecting desktop environments  ")
    api.current_logger().info("==================================")

    # Detect installed desktops by one of the base rpm packages
    kde_desktop_installed = has_package(InstalledRPM, "kde-workspace")
    gnome_desktop_installed = has_package(InstalledRPM, "gnome-session")
    api.current_logger().info("* KDE installed: {0}".format(kde_desktop_installed))
    api.current_logger().info("* Gnome installed: {0}".format(gnome_desktop_installed))
    api.current_logger().info("----------------------------------")

    return {"gnome_installed": gnome_desktop_installed, "kde_installed": kde_desktop_installed}


# repos/system_upgrade/common/actors/getinstalleddesktops/tests/unit_test_getinstalleddesktops.py

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

RH_PACKAGER = 'Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>'
Gnome_RPM = RPM(name='gnome-session', 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')
KDE_RPM = RPM(name='kde-workspace', 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_Gnome_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[Gnome_RPM, ]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledDesktopsFacts)[0]
    assert message.gnome_installed


def test_KDE_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[KDE_RPM, ]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledDesktopsFacts)[0]
    assert message.kde_installed


def test_KDE_Gnome_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[Gnome_RPM, KDE_RPM]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledDesktopsFacts)[0]
    assert message.gnome_installed and message.kde_installed


def test_no_desktop_present(current_actor_context):
    current_actor_context.feed(InstalledRPM(items=[]))
    current_actor_context.run()
    message = current_actor_context.consume(InstalledDesktopsFacts)[0]
    assert not message.gnome_installed and not message.kde_installed


# repos/system_upgrade/common/actors/inhibitwhenluks/tests/test_inhibitwhenluks.py

from leapp.models import CephInfo, LsblkEntry, StorageInfo
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context
from leapp.utils.report import is_inhibitor


def test_actor_with_luks(current_actor_context):
    with_luks = [LsblkEntry(name='luks-132', kname='kname1', maj_min='253:0', rm='0',
                            size='10G', bsize=10*(1 << 39), ro='0', tp='crypt', mountpoint='')]

    current_actor_context.feed(StorageInfo(lsblk=with_luks))
    current_actor_context.run()
    assert current_actor_context.consume(Report)
    report_fields = current_actor_context.consume(Report)[0].report
    assert is_inhibitor(report_fields)


def test_actor_with_luks_ceph_only(current_actor_context):
    with_luks = [LsblkEntry(name='luks-132', kname='kname1', maj_min='253:0', rm='0',
                            size='10G', bsize=10*(1 << 39), ro='0', tp='crypt', mountpoint='')]
    ceph_volume = ['luks-132']
    current_actor_context.feed(StorageInfo(lsblk=with_luks))
    current_actor_context.feed(CephInfo(encrypted_volumes=ceph_volume))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_actor_without_luks(current_actor_context):
    without_luks = [LsblkEntry(name='sda1', kname='sda1', maj_min='8:0', rm='0',
                               size='10G', bsize=10*(1 << 39), ro='0', tp='part', mountpoint='/boot')]

    current_actor_context.feed(StorageInfo(lsblk=without_luks))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/common/actors/initramfs/checkinitramfstasks/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkinitramfstasks
from leapp.models import Report, TargetInitramfsTasks, UpgradeInitramfsTasks
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag


class CheckInitramfsTasks(Actor):
    """
    Inhibit the upgrade if conflicting "initramfs" tasks are detected

    It's possible that some actors could provide conflicting requests
    (e.g. install system dracut module A & custom (from own path) dracut
    module A). This actor prevents system from upgrading if conflicting tasks
    for the upgrade or target initramfses are detected.
    """

    name = 'check_initramfs_tasks'
    consumes = (UpgradeInitramfsTasks, TargetInitramfsTasks)
    produces = (Report,)
    tags = (TargetTransactionChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checkinitramfstasks.process()


# repos/system_upgrade/common/actors/initramfs/mounttargetiso/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import mount_target_iso
from leapp.models import TargetOSInstallationImage, TargetUserSpaceInfo
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class MountTargetISO(Actor):
    """Mounts target OS ISO in order to install upgrade packages from it."""

    name = 'mount_target_iso'
    consumes = (TargetUserSpaceInfo, TargetOSInstallationImage,)
    produces = ()
    tags = (PreparationPhaseTag, IPUWorkflowTag)

    def process(self):
        mount_target_iso.mount_target_iso()


# repos/system_upgrade/common/actors/initramfs/targetinitramfsgenerator/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import targetinitramfsgenerator
from leapp.models import (
    InitrdIncludes,  # deprecated
    InstalledTargetKernelVersion,
    TargetInitramfsTasks
)
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag
from leapp.utils.deprecation import suppress_deprecation


@suppress_deprecation(InitrdIncludes)
class TargetInitramfsGenerator(Actor):
    """
    Regenerate the target RHEL major version initrd and include files produced by other actors
    """

    name = 'target_initramfs_generator'
    consumes = (InitrdIncludes, InstalledTargetKernelVersion, TargetInitramfsTasks)
    produces = ()
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        targetinitramfsgenerator.process()


# repos/system_upgrade/common/actors/initramfs/tmpactor/actor.py

from leapp.actors import Actor
from leapp.models import TargetInitramfsTasks
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag


class TMPActorToSatisfySanityChecks(Actor):
    """
    The actor does NOTHING but satisfy static sanity checks

    The actor is supposed to be removed in future once we resolve issue with
    sanity tests. See https://github.com/oamg/leapp/pull/680 for more details
    about the problems.
    """

    name = 'tmp_actor_to_satisfy_sanity_checks'
    consumes = ()
    produces = (TargetInitramfsTasks,)
    tags = (TargetTransactionChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        return


# repos/system_upgrade/common/actors/initramfs/upgradeinitramfsgenerator/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import upgradeinitramfsgenerator
from leapp.models import RequiredUpgradeInitramPackages  # deprecated
from leapp.models import UpgradeDracutModule  # deprecated
from leapp.models import (
    BootContent,
    TargetOSInstallationImage,
    TargetUserSpaceInfo,
    TargetUserSpaceUpgradeTasks,
    UpgradeInitramfsTasks,
    UsedTargetRepositories
)
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag


class UpgradeInitramfsGenerator(Actor):
    """
    Creates the upgrade initramfs

    Creates an initram disk within a systemd-nspawn container using the target
    system userspace, including new kernel. The creation of the initram disk
    can be influenced with the UpgradeInitramfsTasks message (e.g. specifying
    what files or dracut modules should be installed in the upgrade initramfs)

    See the UpgradeInitramfsTasks model for more details.
    """

    name = 'upgrade_initramfs_generator'
    consumes = (
        RequiredUpgradeInitramPackages,  # deprecated
        TargetOSInstallationImage,
        TargetUserSpaceInfo,
        TargetUserSpaceUpgradeTasks,
        UpgradeDracutModule,  # deprecated
        UpgradeInitramfsTasks,
        UsedTargetRepositories,
    )
    produces = (BootContent,)
    tags = (IPUWorkflowTag, InterimPreparationPhaseTag)

    def process(self):
        upgradeinitramfsgenerator.process()


# repos/system_upgrade/common/actors/ipascanner/libraries/ipascanner.py

import os


def is_ipa_client_configured():
    """
    Check if ipa-client is configured on the system

    :return: True if client is configured
    :rtype: bool
    """
    return all(
        (
            os.path.isfile("/etc/ipa/default.conf"),
            os.path.isfile("/var/lib/ipa-client/sysrestore/sysrestore.state"),
        )
    )


def is_ipa_server_configured():
    """
    Check if ipa-server is configured on the system

    :return: True if server is configured
    :rtype: bool
    """
    return all(
        (
            is_ipa_client_configured(),
            os.path.isfile("/var/lib/ipa/sysrestore/sysrestore.state"),
        )
    )


# repos/system_upgrade/common/actors/ipascanner/tests/test_ipascanner.py

import os

from leapp.models import InstalledRedHatSignedRPM, IpaInfo, RPM
from leapp.snactor.fixture import current_actor_context

DEFAULT_CONF = "/etc/ipa/default.conf"
CLIENT_STATE = "/var/lib/ipa-client/sysrestore/sysrestore.state"
SERVER_STATE = "/var/lib/ipa/sysrestore/sysrestore.state"


def mock_rpm(name):
    return RPM(
        name=name,
        epoch="0",
        packager="Red Hat Inc.",
        version="4.6.0",
        release="1.el7",
        arch="x86_64",
        pgpsig="dummy",
    )


def mock_rpms(*names):
    return InstalledRedHatSignedRPM(items=[mock_rpm(name) for name in names])


def mock_os_path_isfile(overrides):
    def mocked_os_path_isfile(name):
        if name in overrides:
            return overrides[name]
        raise ValueError

    return mocked_os_path_isfile


def assert_ipa_info(infos, client, server):
    assert len(infos) == 1

    info = infos[0]
    assert info.has_client_package == client
    assert info.is_client_configured == client
    assert info.has_server_package == server
    assert info.is_server_configured == server


def test_client_configured(monkeypatch, current_actor_context):
    monkeypatch.setattr(
        "os.path.isfile",
        mock_os_path_isfile(
            {DEFAULT_CONF: True, CLIENT_STATE: True, SERVER_STATE: False}
        ),
    )

    rpms = mock_rpms("ipa-client")
    current_actor_context.feed(rpms)

    current_actor_context.run()

    infos = current_actor_context.consume(IpaInfo)
    assert_ipa_info(infos, True, False)


def test_server_configured(monkeypatch, current_actor_context):
    monkeypatch.setattr(
        "os.path.isfile",
        mock_os_path_isfile(
            {DEFAULT_CONF: True, CLIENT_STATE: True, SERVER_STATE: True}
        ),
    )

    rpms = mock_rpms("ipa-client", "ipa-server")
    current_actor_context.feed(rpms)

    current_actor_context.run()

    infos = current_actor_context.consume(IpaInfo)
    assert_ipa_info(infos, True, True)


def test_not_configured(monkeypatch, current_actor_context):
    monkeypatch.setattr(
        "os.path.isfile",
        mock_os_path_isfile(
            {DEFAULT_CONF: True, CLIENT_STATE: False, SERVER_STATE: False}
        ),
    )

    rpms = mock_rpms()
    current_actor_context.feed(rpms)
    current_actor_context.run()

    infos = current_actor_context.consume(IpaInfo)
    assert_ipa_info(infos, False, False)


# repos/system_upgrade/common/actors/ipuworkflowconfig/libraries/ipuworkflowconfig.py

import os
import platform

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import CalledProcessError, run
from leapp.models import EnvVar, IPUConfig, OSRelease, Version

ENV_IGNORE = ('LEAPP_CURRENT_PHASE', 'LEAPP_CURRENT_ACTOR', 'LEAPP_VERBOSE',
              'LEAPP_DEBUG')

ENV_MAPPING = {'LEAPP_DEVEL_DM_DISABLE_UDEV': 'DM_DISABLE_UDEV'}


def get_env_vars():
    """
    Gather LEAPP_DEVEL environment variables and respective mappings to provide them as messages to be
    available after reboot.
    """
    env_vars = []
    leapp_vars = {k: v for (k, v) in os.environ.items() if k.startswith('LEAPP_') and k not in ENV_IGNORE}
    for k, v in leapp_vars.items():
        if k in ENV_MAPPING:
            env_vars.append(EnvVar(name=ENV_MAPPING.get(k), value=v))
            continue
        env_vars.append(EnvVar(name=k, value=v))

    return env_vars


def get_booted_kernel():
    """
    Get version and release of the currently used kernel in one string.
    """
    try:
        return run(['/usr/bin/uname', '-r'])['stdout'].strip()
    except CalledProcessError as e:
        raise StopActorExecutionError(
            message='Unable to obtain release of the booted kernel.',
            details={'details': str(e), 'stderr': e.stderr}
        )


def get_os_release(path):
    """
    Retrieve data about System OS release from provided file.

    :return: `OSRelease` model if the file can be parsed
    :raises: `IOError`
    """
    try:
        with open(path) as f:
            data = dict(l.strip().split('=', 1) for l in f.readlines() if '=' in l)
            return OSRelease(
                release_id=data.get('ID', '').strip('"'),
                name=data.get('NAME', '').strip('"'),
                pretty_name=data.get('PRETTY_NAME', '').strip('"'),
                version=data.get('VERSION', '').strip('"'),
                version_id=data.get('VERSION_ID', '').strip('"'),
                variant=data.get('VARIANT', '').strip('"') or None,
                variant_id=data.get('VARIANT_ID', '').strip('"') or None
            )
    except IOError as e:
        raise StopActorExecutionError(message='Cannot collect the system OS facts.',
                                      details={'details': str(e)})


def produce_ipu_config(actor):
    flavour = os.environ.get('LEAPP_UPGRADE_PATH_FLAVOUR')
    target_version = os.environ.get('LEAPP_UPGRADE_PATH_TARGET_RELEASE')
    os_release = get_os_release('/etc/os-release')
    actor.produce(IPUConfig(
        leapp_env_vars=get_env_vars(),
        os_release=os_release,
        architecture=platform.machine(),
        version=Version(
            source=os_release.version_id,
            target=target_version
        ),
        kernel=get_booted_kernel(),
        flavour=flavour
    ))


# repos/system_upgrade/common/actors/ipuworkflowconfig/tests/test_ipuworkflowconfig.py

import json
import os
import tempfile

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import ipuworkflowconfig
from leapp.libraries.stdlib import CalledProcessError
from leapp.models import OSRelease

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


def _clean_leapp_envs(monkeypatch):
    """
    Clean all LEAPP environment variables before running the test to have
    fresh env.
    """
    for k, _ in os.environ.items():
        if k.startswith('LEAPP'):
            monkeypatch.delenv(k)


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


def _get_os_release(version='7.9', codename='Maipo'):
    release = OSRelease(
        release_id='rhel',
        name='Red Hat Enterprise Linux Server',
        pretty_name='Red Hat Enterprise Linux',
        version='{} ({})'.format(version, codename),
        version_id='{}'.format(version),
        variant='Server',
        variant_id='server')
    return release


def test_leapp_env_vars(monkeypatch):
    _clean_leapp_envs(monkeypatch)
    monkeypatch.setenv('LEAPP_WHATEVER', '0')
    monkeypatch.setenv('LEAPP_VERBOSE', '1')
    monkeypatch.setenv('LEAPP_DEBUG', '1')
    monkeypatch.setenv('LEAPP_CURRENT_PHASE', 'test')
    monkeypatch.setenv('LEAPP_CURRENT_ACTOR', 'test')
    monkeypatch.setenv('TEST', 'test')
    monkeypatch.setenv('TEST2', 'test')

    assert len(ipuworkflowconfig.get_env_vars()) == 1


def test_get_os_release_info(monkeypatch):
    expected = _get_os_release('7.6')
    assert expected == ipuworkflowconfig.get_os_release(os.path.join(CUR_DIR, 'files/os-release'))

    with pytest.raises(StopActorExecutionError):
        ipuworkflowconfig.get_os_release(os.path.join(CUR_DIR, 'files/non-existent-file'))


def test_get_booted_kernel(monkeypatch):
    monkeypatch.setattr(ipuworkflowconfig, 'run', lambda x: {'stdout': '4.14.0-100.8.2.el7a.x86_64\n'})
    assert ipuworkflowconfig.get_booted_kernel() == '4.14.0-100.8.2.el7a.x86_64'

    monkeypatch.setattr(ipuworkflowconfig, 'run', _raise_call_error)
    with pytest.raises(StopActorExecutionError):
        ipuworkflowconfig.get_booted_kernel()


# repos/system_upgrade/common/actors/kernel/checkinstalledkernels/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkinstalledkernels
from leapp.models import InstalledRedHatSignedRPM
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckInstalledKernels(Actor):
    """
    Inhibit IPU (in-place upgrade) when installed kernels conflict with a safe upgrade.

    a) Inhibit when multiple kernels are installed on a s390x machine

    When on s390x architecture, we are not able to upgrade correctly
    when any kernel is expected to be uninstalled during the rpm
    upgrade transaction now. We discovered recently that removal of
    old kernels is not handled correctly during the IPU. In case the
    maximum number of kernels are installed, the oldest one is
    automatically uninstalled during the rpm upgrade transaction.

    To prevent any related troubles during the IPU, inhibit the IPU
    on s390x unless just one kernel is installed, until the issue will
    be fixed correctly.

    b) Inhibit when machine is not booted into latest installed kernel

    It is strictly required that during the upgrade the machine is
    booted into the latest installed kernel. Upgrading with older
    kernels could cause unexpected issues.
    """

    name = 'check_installed_kernels'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        checkinstalledkernels.process()


# repos/system_upgrade/common/actors/kernelcmdlineconfig/libraries/kernelcmdlineconfig.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries import stdlib
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import InstalledTargetKernelVersion, KernelCmdlineArg, TargetKernelCmdlineArgTasks


def run_grubby_cmd(cmd):
    try:
        stdlib.run(cmd)
        if architecture.matches_architecture(architecture.ARCH_S390X):
            # On s390x we need to call zipl explicitly because of issue in grubby,
            # otherwise the entry is not updated in the ZIPL bootloader
            # See https://bugzilla.redhat.com/show_bug.cgi?id=1764306
            stdlib.run(['/usr/sbin/zipl'])

    except (OSError, stdlib.CalledProcessError) as e:
        raise StopActorExecutionError(
            "Failed to append extra arguments to kernel command line.",
            details={"details": str(e)})


def format_kernelarg_msgs_for_grubby_cmd(kernelarg_msgs):
    kernelarg_msgs = sorted(kernelarg_msgs, key=lambda arg: arg.key)
    kernel_args = []
    for arg in kernelarg_msgs:
        if arg.value:
            kernel_args.append('{0}={1}'.format(arg.key, arg.value))
        else:
            kernel_args.append('{0}'.format(arg.key))
    return ' '.join(kernel_args)


def modify_kernel_args_in_boot_cfg(configs_to_modify_explicitly=None):
    kernel_version = next(api.consume(InstalledTargetKernelVersion), None)
    if not kernel_version:
        return

    # Collect desired kernelopt modifications
    kernelargs_msgs_to_add = list(api.consume(KernelCmdlineArg))
    kernelargs_msgs_to_remove = []
    for target_kernel_arg_task in api.consume(TargetKernelCmdlineArgTasks):
        kernelargs_msgs_to_add.extend(target_kernel_arg_task.to_add)
        kernelargs_msgs_to_remove.extend(target_kernel_arg_task.to_remove)

    if not kernelargs_msgs_to_add and not kernelargs_msgs_to_remove:
        return  # There is no work to do

    grubby_modify_kernelargs_cmd = ['grubby', '--update-kernel=/boot/vmlinuz-{}'.format(kernel_version.version)]

    if kernelargs_msgs_to_add:
        grubby_modify_kernelargs_cmd += [
            '--args', '{}'.format(format_kernelarg_msgs_for_grubby_cmd(kernelargs_msgs_to_add))
        ]

    if kernelargs_msgs_to_remove:
        grubby_modify_kernelargs_cmd += [
            '--remove-args', '{}'.format(format_kernelarg_msgs_for_grubby_cmd(kernelargs_msgs_to_remove))
        ]

    if configs_to_modify_explicitly:
        for config_to_modify in configs_to_modify_explicitly:
            cmd = grubby_modify_kernelargs_cmd + ['-c', config_to_modify]
            run_grubby_cmd(cmd)
    else:
        run_grubby_cmd(grubby_modify_kernelargs_cmd)


# repos/system_upgrade/common/actors/kernelcmdlineconfig/tests/test_kernelcmdlineconfig.py

from collections import namedtuple

import pytest

from leapp.libraries import stdlib
from leapp.libraries.actor import kernelcmdlineconfig
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked
from leapp.libraries.stdlib import api
from leapp.models import InstalledTargetKernelVersion, KernelCmdlineArg, TargetKernelCmdlineArgTasks

KERNEL_VERSION = '1.2.3-4.x86_64.el8'


class MockedRun(object):
    def __init__(self):
        self.commands = []

    def __call__(self, cmd, *args, **kwargs):
        self.commands.append(cmd)
        return {}


@pytest.mark.parametrize(
    ('msgs', 'expected_grubby_kernelopt_args'),
    [
        (
            [KernelCmdlineArg(key='key1', value='value1'), KernelCmdlineArg(key='key2', value='value2')],
            ['--args', 'key1=value1 key2=value2']
        ),
        (
            [TargetKernelCmdlineArgTasks(to_add=[KernelCmdlineArg(key='key1', value='value1'),
                                                 KernelCmdlineArg(key='key2')])],
            ['--args', 'key1=value1 key2']
        ),
        (
            [TargetKernelCmdlineArgTasks(to_add=[KernelCmdlineArg(key='key1', value='value1')]),
             KernelCmdlineArg(key='key2', value='value2')],
            ['--args', 'key1=value1 key2=value2']
        ),
        (
            [TargetKernelCmdlineArgTasks(to_add=[KernelCmdlineArg(key='key1', value='value1')],
                                         to_remove=[KernelCmdlineArg(key='key3')]),
             KernelCmdlineArg(key='key2', value='value2')],
            ['--args', 'key1=value1 key2=value2', '--remove-args', 'key3']
        ),
        (
            [TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='key3'), KernelCmdlineArg(key='key4')])],
            ['--remove-args', 'key3 key4']
        ),
    ]
)
def test_kernelcmdline_config_valid_msgs(monkeypatch, msgs, expected_grubby_kernelopt_args):
    grubby_base_cmd = ['grubby', '--update-kernel=/boot/vmlinuz-{}'.format(KERNEL_VERSION)]
    expected_grubby_cmd = grubby_base_cmd + expected_grubby_kernelopt_args

    mocked_run = MockedRun()
    monkeypatch.setattr(stdlib, 'run', mocked_run)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_X86_64,
                        msgs=[InstalledTargetKernelVersion(version=KERNEL_VERSION)] + msgs))
    kernelcmdlineconfig.modify_kernel_args_in_boot_cfg()
    assert mocked_run.commands and len(mocked_run.commands) == 1
    assert expected_grubby_cmd == mocked_run.commands.pop()

    mocked_run = MockedRun()
    monkeypatch.setattr(stdlib, 'run', mocked_run)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X,
                        msgs=[InstalledTargetKernelVersion(version=KERNEL_VERSION)] + msgs))
    kernelcmdlineconfig.modify_kernel_args_in_boot_cfg()
    assert mocked_run.commands and len(mocked_run.commands) == 2
    assert expected_grubby_cmd == mocked_run.commands.pop(0)
    assert ['/usr/sbin/zipl'] == mocked_run.commands.pop(0)


def test_kernelcmdline_explicit_configs(monkeypatch):
    mocked_run = MockedRun()
    monkeypatch.setattr(stdlib, 'run', mocked_run)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_X86_64,
                        msgs=[InstalledTargetKernelVersion(version=KERNEL_VERSION),
                              TargetKernelCmdlineArgTasks(to_remove=[KernelCmdlineArg(key='key1', value='value1')])]))

    configs = ['/boot/grub2/grub.cfg', '/boot/efi/EFI/redhat/grub.cfg']
    kernelcmdlineconfig.modify_kernel_args_in_boot_cfg(configs_to_modify_explicitly=configs)

    grubby_cmd_without_config = ['grubby', '--update-kernel=/boot/vmlinuz-{}'.format(KERNEL_VERSION),
                                 '--remove-args', 'key1=value1']
    expected_cmds = [
        grubby_cmd_without_config + ['-c', '/boot/grub2/grub.cfg'],
        grubby_cmd_without_config + ['-c', '/boot/efi/EFI/redhat/grub.cfg']
    ]

    assert mocked_run.commands == expected_cmds


def test_kernelcmdline_config_no_args(monkeypatch):
    mocked_run = MockedRun()
    monkeypatch.setattr(stdlib, 'run', mocked_run)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X,
                        msgs=[InstalledTargetKernelVersion(version=KERNEL_VERSION)]))
    kernelcmdlineconfig.modify_kernel_args_in_boot_cfg()
    assert not mocked_run.commands


def test_kernelcmdline_config_no_version(monkeypatch):
    mocked_run = MockedRun()
    monkeypatch.setattr(stdlib, 'run', mocked_run)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(architecture.ARCH_S390X))
    kernelcmdlineconfig.modify_kernel_args_in_boot_cfg()
    assert not mocked_run.commands


# repos/system_upgrade/common/actors/loaddevicedriverdeprecationdata/libraries/deviceanddriverdeprecationdataload.py

import json

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import fetch
from leapp.libraries.stdlib import api
from leapp.models import DeviceDriverDeprecationData, DeviceDriverDeprecationEntry


def _load_file():
    try:
        return json.loads(
            fetch.read_or_fetch('device_driver_deprecation_data.json'))
    except ValueError:
        raise StopActorExecutionError(
            'The device driver deprecation data file is invalid: file does not contain a valid JSON object.',
            details={'hint': ('Read documentation at the following link for more'
                              ' information about how to retrieve the valid file:'
                              ' https://access.redhat.com/articles/3664871')})


def process():
    """
    Loads the device and driver deprecation data and produces a DeviceDriverDeprecationData message with its content.
    It will filter the data on the device_type field, based on the choices set in the StringEnum on the
    DeviceDriverDeprecationEntry model
    """
    # This is how you get the StringEnum choices value, so we can filter based on the model definition
    supported_device_types = set(DeviceDriverDeprecationEntry.device_type.serialize()['choices'])
    api.produce(
        DeviceDriverDeprecationData(
            entries=[
                DeviceDriverDeprecationEntry(**entry)
                for entry in _load_file()['data']
                if entry.get('device_type') in supported_device_types
            ]
        )
    )


# repos/system_upgrade/common/actors/loaddevicedriverdeprecationdata/tests/test_ddddload.py

from leapp.libraries.actor import deviceanddriverdeprecationdataload as ddddload

TEST_DATA = {
    'data': [
        {
            'available_in_rhel': [8, 9],
            'deprecation_announced': '',
            'device_id': 'unsupported:id',
            'device_name': 'Unsupported device type',
            'device_type': 'unsupported',
            'driver_name': '',
            'maintained_in_rhel': [],
        },
        {
            'available_in_rhel': [8, 9],
            'deprecation_announced': '',
            'device_id': 'x86_64:amd:25:1',
            'device_name': 'Supported Family 19h',
            'device_type': 'cpu',
            'driver_name': '',
            'maintained_in_rhel': [],
        },
        {
            'available_in_rhel': [],
            'deprecation_announced': '',
            'device_id': 'x86_64:amd:25:[2-255]',
            'device_name': 'Unsupported Family 19h',
            'device_type': 'cpu',
            'driver_name': '',
            'maintained_in_rhel': [],
        },
        {
            'available_in_rhel': [7],
            'deprecation_announced': '',
            'device_id': '0x10df:0xf180',
            'device_name': 'Emulex Corporation: LPSe12002 EmulexSecure Fibre Channel Adapter',
            'device_type': 'pci',
            'driver_name': 'lpfc',
            'maintained_in_rhel': [7],
        },
    ]
}


def test_filtered_load(monkeypatch):
    produced = []
    monkeypatch.setattr(ddddload, '_load_file', lambda: TEST_DATA)
    monkeypatch.setattr(ddddload.api, 'produce', lambda *v: produced.extend(v))

    ddddload.process()

    assert produced
    assert len(produced[0].entries) == 3
    assert not any([e.device_type == 'unsupported' for e in produced[0].entries])


# repos/system_upgrade/common/actors/localreposinhibit/tests/test_unit_localreposinhibit.py

import pytest

from leapp.models import (
    RepositoryData,
    RepositoryFile,
    TargetOSInstallationImage,
    TMPTargetRepositoriesFacts,
    UsedTargetRepositories,
    UsedTargetRepository
)
from leapp.snactor.fixture import ActorContext


@pytest.mark.parametrize(
    ("baseurl", "mirrorlist", "metalink", "exp_msgs_len"),
    [
        ("file:///root/crb", None, None, 1),
        ("http://localhost/crb", None, None, 0),
        (None, "file:///root/crb", None, 1),
        (None, "http://localhost/crb", None, 0),
        (None, None, "file:///root/crb", 1),
        (None, None, "http://localhost/crb", 0),
        ("http://localhost/crb", "file:///root/crb", None, 1),
        ("file:///root/crb", "http://localhost/crb", None, 0),
        ("http://localhost/crb", None, "file:///root/crb", 1),
        ("file:///root/crb", None, "http://localhost/crb", 0),
    ],
)
def test_unit_localreposinhibit(current_actor_context, baseurl, mirrorlist, metalink, exp_msgs_len):
    """Ensure the Report is generated when local path is used as a baseurl.

    :type current_actor_context: ActorContext
    """
    with pytest.deprecated_call():
        current_actor_context.feed(
            TMPTargetRepositoriesFacts(
                repositories=[
                    RepositoryFile(
                        file="the/path/to/some/file",
                        data=[
                            RepositoryData(
                                name="BASEOS",
                                baseurl=(
                                    "http://example.com/path/to/repo/BaseOS/x86_64/os/"
                                ),
                                repoid="BASEOS",
                            ),
                            RepositoryData(
                                name="APPSTREAM",
                                baseurl=(
                                    "http://example.com/path/to/repo/AppStream/x86_64/os/"
                                ),
                                repoid="APPSTREAM",
                            ),
                            RepositoryData(
                                name="CRB", repoid="CRB", baseurl=baseurl,
                                mirrorlist=mirrorlist, metalink=metalink
                            ),
                        ],
                    )
                ]
            )
        )
    current_actor_context.feed(
        UsedTargetRepositories(
            repos=[
                UsedTargetRepository(repoid="BASEOS"),
                UsedTargetRepository(repoid="CRB"),
            ]
        )
    )
    current_actor_context.run()
    assert len(current_actor_context.messages()) == exp_msgs_len


def test_upgrade_not_inhibited_if_iso_used(current_actor_context):
    repofile = RepositoryFile(file="path/to/some/file",
                              data=[RepositoryData(name="BASEOS", baseurl="file:///path", repoid="BASEOS")])
    current_actor_context.feed(TMPTargetRepositoriesFacts(repositories=[repofile]))
    current_actor_context.feed(UsedTargetRepositories(repos=[UsedTargetRepository(repoid="BASEOS")]))
    current_actor_context.feed(TargetOSInstallationImage(path='', mountpoint='', repositories=[]))


# repos/system_upgrade/common/actors/opensshconfigscanner/libraries/readopensshconfig.py

import errno

from leapp.libraries.common.rpms import check_file_modification
from leapp.libraries.stdlib import api
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin

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 occurrence, which is effective
            if not ret.use_privilege_separation:
                ret.use_privilege_separation = value

        elif el[0].lower() == 'protocol':
            # Record only first occurrence, which is effective
            if not ret.protocol:
                ret.protocol = value

        elif el[0].lower() == 'ciphers':
            # Record only first occurrence, which is effective
            if not ret.ciphers:
                ret.ciphers = value

        elif el[0].lower() == 'macs':
            # Record only first occurrence, which is effective
            if not ret.macs:
                ret.macs = value

        elif el[0].lower() == 'subsystem':
            # Record only first occurrence, which is effective
            if el[1].lower() == 'sftp' and len(el) > 2 and not ret.subsystem_sftp:
                # here we need to record all remaining items as command and arguments
                ret.subsystem_sftp = ' '.join(el[2:])

        elif el[0].lower() in DEPRECATED_DIRECTIVES:
            # Filter out duplicit occurrences 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 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
    config.modified = check_file_modification(CONFIG)

    produce_config(producer, config)


# repos/system_upgrade/common/actors/opensshconfigscanner/tests/test_readopensshconfig_opensshconfigscanner.py

from leapp.libraries.actor.readopensshconfig import line_empty, parse_config, produce_config
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin


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",
        "subsystem sftp internal-sftp",
        "subsystem other internal-other",  # this is ignored
    ]

    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"
    assert output.subsystem_sftp == "internal-sftp"


def test_parse_config_case():
    config = [
        "PermitRootLogin prohibit-password",
        "UsePrivilegeSeparation yes",
        "Protocol 1",
        "SubSystem sftp sftp-server",
    ]

    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"
    assert output.subsystem_sftp == "sftp-server"


def test_parse_config_multiple():
    config = [
        "PermitRootLogin prohibit-password",
        "PermitRootLogin no",
        "PermitRootLogin yes",
        "Ciphers aes128-cbc",
        "Ciphers aes256-cbc",
        "subsystem sftp internal-sftp",
        "subsystem sftp internal-sftp2",
    ]

    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'
    assert output.subsystem_sftp == 'internal-sftp'


def test_parse_config_commented():
    config = [
        "#PermitRootLogin no",
        "#UsePrivilegeSeparation no",
        "#Protocol 12",
        "#SubSystem sftp internal-sftp",
    ]

    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
    assert output.subsystem_sftp is None


def test_parse_config_missing_argument():
    config = [
        "PermitRootLogin",
        "UsePrivilegeSeparation",
        "Protocol"
        "SubSystem"
        "SubSystem sftp"
    ]

    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
    assert output.subsystem_sftp 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_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=[],
        subsystem_sftp="internal-sftp",
    )

    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'
    assert cfg.subsystem_sftp == 'internal-sftp'


def test_actor_execution(current_actor_context):
    current_actor_context.run()
    assert current_actor_context.consume(OpenSshConfig)


# repos/system_upgrade/common/actors/opensshpermitrootlogincheck/libraries/opensshpermitrootlogincheck.py

def global_value(config, default):
    """
    Find the global value for PermitRootLogin option in sshd_config.

    OpenSSH is using the first value found in configuration file, that is not
    in match block other than "all". If there is no such option, the argument
    "default" will be returned.
    """
    for opt in config.permit_root_login:
        if (opt.in_match is None or opt.in_match[0].lower() == 'all'):
            return opt.value
    return default


def semantics_changes(config):
    """
    Check if the current configuration changes semantics if upgraded from RHEL7 to RHEL8

    The case where the configuration does not contain *any* PermitRootLogin option is
    already covered in the actor and does not need to be handled here.

    This tries to capture the case, where the root login is enabled in at least one
    match block. The global default changes so the new configurations will not allow
    all password root logins, but there is at least some chance to access the system as
    root with password.

    Examples:
    * If the root login is globally set (enabled or disabled), the semantics stays the same.
    * If the root login is enabled only in match blocks, the semantics changes, but the
      machine stays accessible at least for clients matching this block.

    """
    config_global_value = global_value(config, None)
    in_match_enabled = False
    if not config.permit_root_login:
        return True

    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_enabled = True

    return config_global_value is None and not in_match_enabled


# repos/system_upgrade/common/actors/opensshpermitrootlogincheck/tests/test_library_opensshpermitrootlogincheck.py

from leapp.libraries.actor.opensshpermitrootlogincheck import global_value, semantics_changes
from leapp.models import OpenSshConfig, OpenSshPermitRootLogin


def test_empty_file():
    """ Empty file
    """
    config = OpenSshConfig(
        permit_root_login=[
        ],
        deprecated_directives=[]
    )

    assert semantics_changes(config)
    assert global_value(config, "default") == "default"


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)
    assert global_value(config, "default") == "yes"


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)
    assert global_value(config, "default") == "no"


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)
    assert global_value(config, "default") == "prohibit-password"


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)
    assert global_value(config, "default") == "default"


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)
    assert global_value(config, "default") == "default"


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)
    assert global_value(config, "default") == "default"


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)
    assert global_value(config, "default") == "no"


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)
    assert global_value(config, "default") == "prohibit-password"


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)
    assert global_value(config, "default") == "yes"


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)
    assert global_value(config, "default") == "no"


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)
    assert global_value(config, "default") == "yes"


# repos/system_upgrade/common/actors/pcidevicesscanner/libraries/pcidevicesscanner.py

import re

from leapp.libraries.stdlib import api, run
from leapp.models import (
    ActiveKernelModulesFacts,
    DetectedDeviceOrDriver,
    DeviceDriverDeprecationData,
    PCIDevice,
    PCIDevices
)

# Regex to capture Vendor, Device and SVendor and SDevice values
PCI_ID_REG = re.compile(r"(?<=Vendor:\t|Device:\t)\w+")


# TODO this could be solved more efficiently and error prune via python re
#   and groupdict
def parse_pci_device(textual_block, numeric_block):
    """ Parse one block from lspci output describing one PCI device """
    device = {
        'Slot': '',
        'Class': '',
        'Vendor': '',
        'Device': '',
        'SVendor': '',
        'SDevice': '',
        'PhySlot': '',
        'Rev': '',
        'ProgIf': '',
        'Driver': '',
        'Module': [],
        'NUMANode': ''
    }
    for line in textual_block.splitlines():
        key, value = line.split(':\t')

        if key in device:
            if isinstance(device[key], list):
                device[key].append(value)
            else:
                if device[key]:
                    api.current_logger().debug(
                        'Unexpected duplicate key - {k}: {v} (current value: {vcurr}), ignoring'.format(
                            k=key, v=value, vcurr=device[key]))
                else:
                    device[key] = value
        else:
            api.current_logger().debug('Unrecognized key - {k}: {v}, ignoring'.format(k=key, v=value))

    return PCIDevice(
        slot=device['Slot'],
        dev_cls=device['Class'],
        vendor=device['Vendor'],
        name=device['Device'],
        subsystem_vendor=device['SVendor'],
        subsystem_name=device['SDevice'],
        physical_slot=device['PhySlot'],
        rev=device['Rev'],
        progif=device['ProgIf'],
        driver=device['Driver'],
        modules=device['Module'],
        numa_node=device['NUMANode'],
        pci_id=":".join(PCI_ID_REG.findall(numeric_block))
    )


def parse_pci_devices(pci_textual, pci_numeric):
    """ Parse lspci output and return a list of PCI devices """
    return [
        parse_pci_device(*block) for block
        in zip(
            pci_textual.split('\n\n')[:-1],
            pci_numeric.split('\n\n')[:-1]
        )
    ]


def produce_detected_devices(devices):
    prefix_re = re.compile('0x')
    entry_lookup = {
        prefix_re.sub('', entry.device_id): entry
        for message in api.consume(DeviceDriverDeprecationData) for entry in message.entries
    }
    api.produce(*[
        DetectedDeviceOrDriver(**entry_lookup[device.pci_id].dump())
        for device in devices
        if device.pci_id in entry_lookup
    ])


def produce_detected_drivers(devices):
    active_modules = {
        module.file_name
        for message in api.consume(ActiveKernelModulesFacts) for module in message.kernel_modules
    }

    # Create a lookup by driver_name and filter out the kernel that are active
    entry_lookup = {
        entry.driver_name: entry
        for message in api.consume(DeviceDriverDeprecationData) for entry in message.entries
        if not entry.device_id and entry.driver_name and entry.driver_name not in active_modules
    }

    drivers = {device.driver for device in devices if device.driver in entry_lookup}
    api.produce(*[
        DetectedDeviceOrDriver(**entry_lookup[driver].dump())
        for driver in drivers
    ])


def produce_pci_devices(producer, devices):
    """ Produce a Leapp message with all PCI devices """
    producer(PCIDevices(devices=devices))


def scan_pci_devices(producer):
    """ Scan system PCI Devices """
    pci_textual = run(['lspci', '-vmmk'], checked=False)['stdout']
    pci_numeric = run(['lspci', '-vmmkn'], checked=False)['stdout']
    devices = parse_pci_devices(pci_textual, pci_numeric)
    produce_detected_devices(devices)
    produce_detected_drivers(devices)
    produce_pci_devices(producer, devices)


# repos/system_upgrade/common/actors/pcidevicesscanner/tests/test_pcidevicesscanner.py

from leapp.libraries.actor.pcidevicesscanner import parse_pci_devices, produce_pci_devices
from leapp.models import PCIDevice, PCIDevices


def test_parse_pci_devices():
    devices_textual = '''Slot:	00:00.0
Class:	Host bridge
Vendor:	Intel Corporation
Device:	440FX - 82441FX PMC [Natoma]
SVendor:	Red Hat, Inc.
SDevice:	Qemu virtual machine
PhySlot:	3
Rev:	02
NUMANode:	0

Slot:	00:01.0
Class:	ISA bridge
Vendor:	Intel Corporation
Device:	82371SB PIIX3 ISA [Natoma/Triton II]
SVendor:	Red Hat, Inc.
SDevice:	Qemu virtual machine

Slot:	00:01.1
Class:	IDE interface
Vendor:	Intel Corporation
Device:	82371SB PIIX3 IDE [Natoma/Triton II]
ProgIf:	80
Driver:	ata_piix
Module:	ata_piix
Module:	pata_acpi
Module:	ata_generic

'''
    devices_numeric = '''Slot:	00:00.0
Class:	Host bridge
Vendor:	15b45
Device:	0724
SVendor:	15b46
SDevice:	0725
PhySlot:	3
Rev:	02
NUMANode:	0

Slot:	00:01.0
Class:	ISA bridge
Vendor:	15b44
Device:	0723
SVendor:	15b50
SDevice:	0750

Slot:	00:01.1
Class:	IDE interface
Vendor:	15b43
Device:	0722
ProgIf:	80
Driver:	ata_piix
Module:	ata_piix
Module:	pata_acpi
Module:	ata_generic

'''

    output = parse_pci_devices(devices_textual, devices_numeric)
    assert isinstance(output, list)
    assert len(output) == 3

    dev = output.pop()
    assert dev.slot == '00:01.1'
    assert dev.dev_cls == 'IDE interface'
    assert dev.vendor == 'Intel Corporation'
    assert dev.name == '82371SB PIIX3 IDE [Natoma/Triton II]'
    assert dev.progif == '80'
    assert dev.driver == 'ata_piix'
    assert dev.pci_id == '15b43:0722'
    assert len(dev.modules) == 3
    assert 'ata_piix' in dev.modules
    assert 'pata_acpi' in dev.modules
    assert 'ata_generic' in dev.modules

    dev = output.pop()
    assert dev.slot == '00:01.0'
    assert dev.dev_cls == 'ISA bridge'
    assert dev.vendor == 'Intel Corporation'
    assert dev.name == '82371SB PIIX3 ISA [Natoma/Triton II]'
    assert dev.subsystem_vendor == 'Red Hat, Inc.'
    assert dev.subsystem_name == 'Qemu virtual machine'
    assert dev.driver == ''
    assert dev.modules == []
    assert dev.rev == ''
    assert dev.physical_slot == ''
    assert dev.numa_node == ''
    assert dev.pci_id == '15b44:0723:15b50:0750'

    dev = output.pop()
    assert dev.slot == '00:00.0'
    assert dev.dev_cls == 'Host bridge'
    assert dev.vendor == 'Intel Corporation'
    assert dev.name == '440FX - 82441FX PMC [Natoma]'
    assert dev.subsystem_vendor == 'Red Hat, Inc.'
    assert dev.subsystem_name == 'Qemu virtual machine'
    assert dev.rev == '02'
    assert dev.physical_slot == '3'
    assert dev.numa_node == '0'
    assert dev.pci_id == '15b45:0724:15b46:0725'


def test_parse_empty_list():
    output = parse_pci_devices('', '')
    assert isinstance(output, list)
    assert not output


def test_parse_unknown_keys():
    devices_textual = '''Slot:	00:1c.0
Class:	PCI bridge
Material:	Silicon
Vendor:	Intel Corporation
Origin:	People's Republic of China
Device:	Sunrise Point-LP PCI Express Root Port #1
Flavor:	Burnt toast
Rev:	f1
Flavor:	Spicy beef
Driver:	pcieport

'''
    devices_numeric = '''Slot:	00:1c.0
Class:	PCI bridge
Material:	Silicon
Vendor:	15b74
Origin:	People's Republic of China
Device:	0724
Flavor:	Burnt toast
Rev:	f1
Flavor:	Spicy beef
Driver:	pcieport

'''

    output = parse_pci_devices(devices_textual, devices_numeric)
    assert isinstance(output, list)
    assert len(output) == 1

    dev = output.pop()
    assert dev.slot == '00:1c.0'
    assert dev.dev_cls == 'PCI bridge'
    assert dev.vendor == 'Intel Corporation'
    assert dev.name == 'Sunrise Point-LP PCI Express Root Port #1'
    assert dev.rev == 'f1'
    assert dev.driver == 'pcieport'
    assert dev.pci_id == '15b74:0724'
    assert dev.modules == []


def test_produce_pci_devices():
    output = []

    def fake_producer(*args):
        output.extend(args)

    devices = [
        PCIDevice(
            slot='00:00.0',
            dev_cls='Host bridge',
            vendor='Intel Corporation',
            name='440FX - 82441FX PMC [Natoma]',
            subsystem_vendor='Red Hat, Inc.',
            subsystem_name='Qemu virtual machine',
            pci_id='15b560:0739',
            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'),
    ]

    produce_pci_devices(fake_producer, devices)
    assert len(output) == 1
    assert len(output[0].devices) == 3


def test_produce_no_devices():
    output = []

    def fake_producer(*args):
        output.extend(args)

    produce_pci_devices(fake_producer, [])
    assert len(output) == 1
    assert not output[0].devices


def test_actor_execution(current_actor_context):
    current_actor_context.run()
    assert current_actor_context.consume(PCIDevices)


# repos/system_upgrade/common/actors/persistentnetnames/tests/test_persistentnetnames.py

import pytest

from leapp.libraries.common import persistentnetnames
from leapp.models import Interface, PCIAddress, PersistentNetNamesFacts
from leapp.snactor.fixture import current_actor_context


def interface_mocked(i=0):
    return Interface(
        name='n{}'.format(i),
        devpath='dp{}'.format(i),
        driver='d{}'.format(i),
        vendor='v{}'.format(i),
        pci_info=PCIAddress(
            domain='pd{}'.format(i),
            bus='pb{}'.format(i),
            function='pf{}'.format(i),
            device='pd{}'.format(i)
        ),
        mac='m{}'.format(i)
    )


class interfaces_mocked(object):
    def __init__(self, count):
        self.count = count

    def __call__(self):
        for i in range(self.count):
            yield interface_mocked(i)


@pytest.mark.parametrize('count', [0, 1, 8, 256])
def test_run(monkeypatch, current_actor_context, count):
    monkeypatch.setattr(persistentnetnames, 'interfaces', interfaces_mocked(count))
    current_actor_context.run()
    assert len(current_actor_context.consume(PersistentNetNamesFacts)[0].interfaces) == count


# repos/system_upgrade/common/actors/persistentnetnamesconfig/libraries/persistentnetnamesconfig.py

import errno
import os
import re

from leapp.libraries.common.config import get_env
from leapp.libraries.stdlib import api
from leapp.models import (
    InitrdIncludes,
    PersistentNetNamesFacts,
    PersistentNetNamesFactsInitramfs,
    RenamedInterface,
    RenamedInterfaces,
    TargetInitramfsTasks
)
from leapp.utils.deprecation import suppress_deprecation

LINK_FILE_TEMPLATE = """# Generated by LEAPP
[Match]
MACAddress={}

[Link]
Name={}
"""


def generate_link_file(interface):
    try:
        os.makedirs('/etc/systemd/network')
    except OSError as e:
        if e.errno != errno.EEXIST:
            raise

    link_file = '/etc/systemd/network/10-leapp-{}.link'.format(interface.name)
    with open(link_file, 'w') as f:
        f.write(LINK_FILE_TEMPLATE.format(interface.mac, interface.name))

    return link_file


@suppress_deprecation(InitrdIncludes)
def process():

    if get_env('LEAPP_NO_NETWORK_RENAMING', '0') == '1':
        api.current_logger().info(
            'Skipping handling of possibly renamed network interfaces: leapp executed with LEAPP_NO_NETWORK_RENAMING=1'
        )
        return

    rhel7_ifaces = next(api.consume(PersistentNetNamesFacts)).interfaces
    rhel8_ifaces = next(api.consume(PersistentNetNamesFactsInitramfs)).interfaces

    rhel7_ifaces_map = {iface.mac: iface for iface in rhel7_ifaces}
    rhel8_ifaces_map = {iface.mac: iface for iface in rhel8_ifaces}

    initrd_files = []
    missing_ifaces = []
    renamed_interfaces = []

    if rhel7_ifaces != rhel8_ifaces:
        for iface in rhel7_ifaces:
            rhel7_name = rhel7_ifaces_map[iface.mac].name
            try:
                rhel8_name = rhel8_ifaces_map[iface.mac].name
            except KeyError:
                missing_ifaces.append(iface)
                api.current_logger().warning(
                    'The device with MAC "{}" is not detected in the upgrade'
                    ' environment. Required driver: "{}".'
                    ' Original interface name: "{}".'
                    .format(iface.mac, iface.driver, iface.name)
                )
                continue

            if rhel7_name != rhel8_name and get_env('LEAPP_NO_NETWORK_RENAMING', '0') != '1':
                api.current_logger().warning('Detected interface rename {} -> {}.'.format(rhel7_name, rhel8_name))

                if re.search('eth[0-9]+', iface.name) is not None:
                    api.current_logger().warning('Interface named using eth prefix, refusing to generate link file')
                    renamed_interfaces.append(RenamedInterface(**{'rhel7_name': rhel7_name,
                                                                  'rhel8_name': rhel8_name}))
                    continue

                initrd_files.append(generate_link_file(iface))

    if missing_ifaces:
        msg = (
            'Some network devices have not been detected inside the'
            ' upgrade environment and so related network interfaces'
            ' could be renamed on the upgraded system.'
        )
        # Note(pstodulk):
        # This usually happens when required (RHEL 8 compatible)
        # drivers are not included in the upgrade initramfs.
        # We can add more information later. Currently we cannot provide
        # better instructions for users before (at least):
        # a) networking work in the upgrade initramfs (PR #583)
        # b) it's possible to influence the upgrade initramfs (PR #517)
        # TODO(pstodulk): gen report msg
        api.current_logger().warning(msg)

    api.produce(RenamedInterfaces(renamed=renamed_interfaces))
    api.produce(InitrdIncludes(files=initrd_files))
    # TODO: cover actor by tests in future. I am skipping writing of tests
    # now as some refactoring and bugfixing related to this actor
    # is planned already.
    api.produce(TargetInitramfsTasks(include_files=initrd_files))


# repos/system_upgrade/common/actors/persistentnetnamesconfig/tests/test_persistentnetnamesconfig.py

import json
import os

import pytest

from leapp.libraries.actor import persistentnetnamesconfig
from leapp.libraries.common.config import mock_configs
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
from leapp.models import (
    InitrdIncludes,
    Interface,
    PCIAddress,
    PersistentNetNamesFacts,
    PersistentNetNamesFactsInitramfs,
    RenamedInterface,
    RenamedInterfaces,
    TargetInitramfsTasks
)

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


@pytest.fixture
def adjust_cwd():
    previous_cwd = os.getcwd()
    os.chdir(TEST_DIR)
    yield
    os.chdir(previous_cwd)


def generate_link_file_mocked(interface):
    return '/etc/systemd/network/10-leapp-{}.link'.format(interface.name)


def interface_mocked(i=0):
    return Interface(
        name='n{}'.format(i),
        devpath='dp{}'.format(i),
        driver='d{}'.format(i),
        vendor='v{}'.format(i),
        pci_info=PCIAddress(
            domain='pd{}'.format(i),
            bus='pb{}'.format(i),
            function='pf{}'.format(i),
            device='pd{}'.format(i)
        ),
        mac='m{}'.format(i)
    )


def generate_interfaces(count):
    return [interface_mocked(i) for i in range(count)]


def test_identical(current_actor_context):
    interfaces = generate_interfaces(4)
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
    current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
    current_actor_context.run(config_model=mock_configs.CONFIG)

    renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
    initrd_files = current_actor_context.consume(InitrdIncludes)[0]
    t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
    assert initrd_files.files == t_initrafms_tasks.include_files
    assert not renamed_interfaces.renamed
    assert not t_initrafms_tasks.include_files


def test_renamed_single_noneth(monkeypatch, current_actor_context):
    monkeypatch.setattr(persistentnetnamesconfig, 'generate_link_file', generate_link_file_mocked)

    interfaces = generate_interfaces(4)
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
    interfaces[0].name = 'n4'
    current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
    current_actor_context.run(config_model=mock_configs.CONFIG)

    renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
    initrd_files = current_actor_context.consume(InitrdIncludes)[0]
    t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
    assert initrd_files.files == t_initrafms_tasks.include_files
    assert not renamed_interfaces.renamed
    assert len(t_initrafms_tasks.include_files) == 1
    assert '/etc/systemd/network/10-leapp-n0.link' in t_initrafms_tasks.include_files


def test_renamed_swap_noneth(monkeypatch, current_actor_context):
    monkeypatch.setattr(persistentnetnamesconfig, 'generate_link_file', generate_link_file_mocked)

    interfaces = generate_interfaces(4)
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
    interfaces[0].name = 'n3'
    interfaces[3].name = 'n0'
    current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
    current_actor_context.run(config_model=mock_configs.CONFIG)

    renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
    initrd_files = current_actor_context.consume(InitrdIncludes)[0]
    t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
    assert initrd_files.files == t_initrafms_tasks.include_files
    assert not renamed_interfaces.renamed
    assert len(t_initrafms_tasks.include_files) == 2
    assert '/etc/systemd/network/10-leapp-n0.link' in t_initrafms_tasks.include_files
    assert '/etc/systemd/network/10-leapp-n3.link' in t_initrafms_tasks.include_files


def test_renamed_single_eth(monkeypatch, current_actor_context):
    monkeypatch.setattr(persistentnetnamesconfig, 'generate_link_file', generate_link_file_mocked)

    interfaces = generate_interfaces(4)
    for i in range(4):
        interfaces[i].name = 'eth{}'.format(i)
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
    interfaces[0].name = 'eth4'
    current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
    current_actor_context.run(config_model=mock_configs.CONFIG)

    renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
    initrd_files = current_actor_context.consume(InitrdIncludes)[0]
    t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
    assert initrd_files.files == t_initrafms_tasks.include_files
    assert len(renamed_interfaces.renamed) == 1
    assert renamed_interfaces.renamed[0].rhel7_name == 'eth0'
    assert renamed_interfaces.renamed[0].rhel8_name == 'eth4'
    assert not t_initrafms_tasks.include_files


def test_renamed_swap_eth(monkeypatch, current_actor_context):
    monkeypatch.setattr(persistentnetnamesconfig, 'generate_link_file', generate_link_file_mocked)

    interfaces = generate_interfaces(4)
    for i in range(4):
        interfaces[i].name = 'eth{}'.format(i)
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interfaces))
    interfaces[0].name = 'eth3'
    interfaces[3].name = 'eth0'
    current_actor_context.feed(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
    current_actor_context.run(config_model=mock_configs.CONFIG)

    renamed_interfaces = current_actor_context.consume(RenamedInterfaces)[0]
    initrd_files = current_actor_context.consume(InitrdIncludes)[0]
    t_initrafms_tasks = current_actor_context.consume(TargetInitramfsTasks)[0]
    assert initrd_files.files == t_initrafms_tasks.include_files
    assert len(renamed_interfaces.renamed) == 2
    for interface in renamed_interfaces.renamed:
        if interface.rhel7_name == 'eth0':
            assert interface.rhel8_name == 'eth3'
        elif interface.rhel7_name == 'eth3':
            assert interface.rhel8_name == 'eth0'
    assert not t_initrafms_tasks.include_files


def test_bz_1899455_crash_iface(monkeypatch, adjust_cwd):
    """
    Cover situation when network device is discovered on the src sys but not
    inside the upgrade environment.

    This typically happens when the network device needs specific drivers which
    are not present inside the upgrade initramfs. Usually it points to a missing
    actors that should influence the upgrade initramfs in a way the drivers are
    installed. In this situation, only correct thing we can do in this actor
    is print warning / report that we couldn't located particular devices so
    we cannot handle interface names related to this devices.
    """
    with open(os.path.join(CUR_DIR, 'files/crashed_ifaces.json')) as fp:
        json_msgs = json.load(fp)
    msgs = [
        PersistentNetNamesFacts.create(json_msgs["PersistentNetNamesFacts"]),
        PersistentNetNamesFactsInitramfs.create(json_msgs["PersistentNetNamesFactsInitramfs"]),
    ]
    monkeypatch.setattr(persistentnetnamesconfig, 'generate_link_file', generate_link_file_mocked)
    monkeypatch.setattr(persistentnetnamesconfig.api, 'current_actor', CurrentActorMocked(msgs=msgs))
    monkeypatch.setattr(persistentnetnamesconfig.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(persistentnetnamesconfig.api, 'produce', produce_mocked())
    persistentnetnamesconfig.process()

    for prod_models in [RenamedInterfaces, InitrdIncludes, TargetInitramfsTasks]:
        any(isinstance(i, prod_models) for i in persistentnetnamesconfig.api.produce.model_instances)
    assert any(['Some network devices' in x for x in persistentnetnamesconfig.api.current_logger.warnmsg])


def test_no_network_renaming(monkeypatch):
    """
    This should cover OAMG-4243.
    """
    # this mock should be needed, as this function should be called, but just
    # for a check..
    monkeypatch.setattr(persistentnetnamesconfig, 'generate_link_file', generate_link_file_mocked)

    interfaces = generate_interfaces(4)
    for i in range(4):
        interfaces[i].name = 'myinterface{}'.format(i)
    msgs = [PersistentNetNamesFacts(interfaces=interfaces)]
    interfaces[0].name = 'changedinterfacename0'
    msgs.append(PersistentNetNamesFactsInitramfs(interfaces=interfaces))
    mocked_actor = CurrentActorMocked(msgs=msgs, envars={'LEAPP_NO_NETWORK_RENAMING': '1'})
    monkeypatch.setattr(persistentnetnamesconfig.api, 'current_actor', mocked_actor)
    monkeypatch.setattr(persistentnetnamesconfig.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(persistentnetnamesconfig.api, 'produce', produce_mocked())
    persistentnetnamesconfig.process()

    ilog = 'Skipping handling of possibly renamed network interfaces: leapp executed with LEAPP_NO_NETWORK_RENAMING=1'
    assert ilog in persistentnetnamesconfig.api.current_logger.infomsg
    assert not persistentnetnamesconfig.api.produce.called


# repos/system_upgrade/common/actors/persistentnetnamesdisable/tests/test_persistentnetnamesdisable.py

from leapp.models import Interface, KernelCmdlineArg, PCIAddress, PersistentNetNamesFacts
from leapp.reporting import Report
from leapp.snactor.fixture import current_actor_context
from leapp.utils.report import is_inhibitor


def test_actor_single_eth0(current_actor_context):
    pci = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
    interface = [Interface(name="eth0", mac="52:54:00:0b:4a:6d", vendor="redhat",
                           driver="pcieport", pci_info=pci,
                           devpath="/devices/platform/usb/cdc-wdm0")]
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_actor_more_ethX(current_actor_context):
    pci1 = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
    pci2 = PCIAddress(domain="0000", bus="3d", function="00", device="Serial controller")
    interface = [Interface(name="eth0", mac="52:54:00:0b:4a:6d", vendor="redhat",
                           driver="pcieport", pci_info=pci1,
                           devpath="/devices/platform/usb/cdc-wdm0"),
                 Interface(name="eth1", mac="52:54:00:0b:4a:6a", vendor="redhat",
                           driver="serial", pci_info=pci2,
                           devpath="/devices/hidraw/hidraw0")]
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
    current_actor_context.run()
    assert current_actor_context.consume(Report)
    assert is_inhibitor(current_actor_context.consume(Report)[0].report)


def test_actor_single_int_not_ethX(current_actor_context):
    pci = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
    interface = [Interface(name="tap0", mac="52:54:00:0b:4a:60", vendor="redhat",
                           driver="pcieport", pci_info=pci,
                           devpath="/devices/platform/usb/cdc-wdm0")]
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


def test_actor_ethX_and_not_ethX(current_actor_context):
    pci1 = PCIAddress(domain="0000", bus="3e", function="00", device="PCI bridge")
    pci2 = PCIAddress(domain="0000", bus="3d", function="00", device="Serial controller")
    interface = [Interface(name="virbr0", mac="52:54:00:0b:4a:6d", vendor="redhat",
                           driver="pcieport", pci_info=pci1,
                           devpath="/devices/platform/usb/cdc-wdm0"),
                 Interface(name="eth0", mac="52:54:00:0b:4a:6a", vendor="redhat",
                           driver="serial", pci_info=pci2,
                           devpath="/devices/hidraw/hidraw0")]
    current_actor_context.feed(PersistentNetNamesFacts(interfaces=interface))
    current_actor_context.run()
    assert current_actor_context.consume(Report)
    assert is_inhibitor(current_actor_context.consume(Report)[0].report)


# repos/system_upgrade/common/actors/persistentnetnamesinitramfs/tests/test_persistentnetnamesinitramfs.py

import pytest

from leapp.libraries.common import persistentnetnames
from leapp.models import Interface, PCIAddress, PersistentNetNamesFactsInitramfs
from leapp.snactor.fixture import current_actor_context


def interface_mocked(i=0):
    return Interface(
        name='n{}'.format(i),
        devpath='dp{}'.format(i),
        driver='d{}'.format(i),
        vendor='v{}'.format(i),
        pci_info=PCIAddress(
            domain='pd{}'.format(i),
            bus='pb{}'.format(i),
            function='pf{}'.format(i),
            device='pd{}'.format(i)
        ),
        mac='m{}'.format(i)
    )


class interfaces_mocked(object):
    def __init__(self, count):
        self.count = count

    def __call__(self):
        for i in range(self.count):
            yield interface_mocked(i)


@pytest.mark.parametrize('count', [0, 1, 8, 256])
def test_run(monkeypatch, current_actor_context, count):
    monkeypatch.setattr(persistentnetnames, 'interfaces', interfaces_mocked(count))
    current_actor_context.run()
    assert len(current_actor_context.consume(PersistentNetNamesFactsInitramfs)[0].interfaces) == count


# repos/system_upgrade/common/actors/peseventsscanner/libraries/pes_event_parsing.py

import json
import os
from collections import defaultdict, namedtuple
from enum import IntEnum
from itertools import chain

from leapp import reporting
from leapp.exceptions import StopActorExecution
from leapp.libraries.common import fetch
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api

# NOTE(mhecko): The modulestream field contains a set of modulestreams until the very end when we generate a Package
# for every modulestream in this set.
_Package = namedtuple('Package', ['name',         # str
                                  'repository',   # str
                                  'modulestream'  # (str, str) or None
                                  ])


class Package(_Package):  # noqa: E0102; pylint: disable=function-redefined
    def __repr__(self):
        ms = ''
        if self.modulestream:
            # The package class contains multiple modulestreams until a package for every single of these modulestreams
            # is generated. We have to check whether modulestream in fact contains multiple modulestreams and handle it
            # accordingly, so that we print Package when debugging.
            if len(self.modulestream) == 2 and all((isinstance(item, str) for item in self.modulestream)):
                ms = '@{0}:{1}'.format(*self.modulestream)
            else:
                ms = '{{{0}}}'.format(','.join(str(item) for item in self.modulestream))
        return '{n}:{r}{ms}'.format(n=self.name, r=self.repository, ms=ms)

    def __hash__(self):
        return hash((self.name, self.modulestream))

    def __eq__(self, other):
        return (self.name, self.modulestream) == (other.name, other.modulestream)


Event = namedtuple('Event', ['id',            # int
                             'action',        # An instance of Action
                             'in_pkgs',       # A set of Package named tuples
                             'out_pkgs',      # A set of Package named tuples
                             'from_release',  # A tuple representing a release in format (major, minor)
                             'to_release',    # A tuple representing a release in format (major, minor)
                             'architectures'  # A list of strings representing architectures
                             ])


class Action(IntEnum):
    PRESENT = 0
    REMOVED = 1
    DEPRECATED = 2
    REPLACED = 3
    SPLIT = 4
    MERGED = 5
    MOVED = 6
    RENAMED = 7


def get_pes_events(pes_json_directory, pes_json_filename):
    """
    Get all the events from the source JSON file exported from PES.

    :return: List of Event tuples, where each event contains event type and input/output pkgs
    """
    try:
        all_events = parse_pes_events(fetch.read_or_fetch(pes_json_filename, directory=pes_json_directory,
                                                          allow_empty=True))
        arch = api.current_actor().configuration.architecture
        events_matching_arch = [e for e in all_events if not e.architectures or arch in e.architectures]
        return events_matching_arch
    except (ValueError, KeyError):
        title = 'Missing/Invalid PES data file ({}/{})'.format(pes_json_directory, pes_json_filename)
        summary = ('Read documentation at: https://access.redhat.com/articles/3664871 for more information ',
                   'about how to retrieve the files')
        reporting.create_report([
            reporting.Title(title),
            reporting.Summary(summary),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.SANITY]),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.RelatedResource('file', os.path.join(pes_json_directory, pes_json_filename))
        ])
        raise StopActorExecution()


def generate_event_for_ms_mapping_entry(from_ms_to_ms_entry, event):
    from_modulestream, to_modulestreams = from_ms_to_ms_entry

    in_pkgs_matching_from_ms = {
        Package(p.name, p.repository, from_modulestream) for p in event.in_pkgs if from_modulestream in p.modulestream
    }

    # Out pkgs are a bit harder to pick, as there are more than one modulestreams in to_modulestreams
    out_pkgs_matching_to_ms = set()
    for pkg in event.out_pkgs:
        target_modulestreams = set(pkg.modulestream).intersection(to_modulestreams)
        if len(target_modulestreams) > 1:
            # This is not expected to happen. Produce a warning, but continue by picking a single of the modulestreams.
            api.current_logger().warning('Event {0} - output package {1} matches multiple to_modulestreams '
                                         ' when generating events per modulestream mapping.')
        if not target_modulestreams:
            # No target_modulestreams mean that this package should not be included in the resulting event
            continue

        target_modulestream = next(iter(target_modulestreams))
        out_pkgs_matching_to_ms.add(Package(pkg.name, pkg.repository, target_modulestream))

    return Event(
        event.id,
        event.action,
        in_pkgs_matching_from_ms,
        out_pkgs_matching_to_ms,
        event.from_release,
        event.to_release,
        event.architectures
    )


def event_by_modulestream_mapping(mapping, event):
    """
    Generate events with matching event.ID according to the the given modulestream mapping.

    Modulestream mapping allows for a compact symbolic representation of multiple events having the same input/output
    packages differing only in the modules they are in. For example consider event renaming package X with module
    streams [MSX0, MSX1], to a new package with the name Y and possible module streams [MSY0, MSY1]. The attached
    modulestream map {MSX0 -> MSY0, MSX1 -> MSY1} would mean that the renaming event represents two events --- one
    renaming (X, ms=MSX0) to (Y, ms=MSY0), and the second one renaming (X, ms=MSX1) to (Y, ms=MSY1).

    :param mapping: A dictionary mapping package's input modulestream to its output modulestreams.
    :param event: PES event that should be uncompressed.
    :returns: A list of generated PES events.
    """
    if not mapping:
        # In case there is no mapping, assume all is going to non modular
        mapping = defaultdict(set)
        for package in event.in_pkgs:
            for ms in package.modulestream or ():
                mapping[ms].add(None)

    return [generate_event_for_ms_mapping_entry(from_ms_to_ms_entry, event) for from_ms_to_ms_entry in mapping.items()]


def parse_pes_events(json_data):
    """
    Parse JSON data returning PES events

    :return: List of Event tuples, where each event contains event type and input/output pkgs
    """
    data = json.loads(json_data)
    if not isinstance(data, dict) or not data.get('packageinfo'):
        raise ValueError('Found PES data with invalid structure')

    return list(chain(*[parse_entry(entry) for entry in data['packageinfo']]))


def parse_entry(entry):
    """
    Parse PES event data

    :param entry: A dict with the following structure:
                  {
                      'action': 3,
                      'id': 15,
                      'initial_release': {  # can be None
                          'z_stream': None,
                          'major_version': 7,
                          'tag': None,
                          'os_name': 'RHEL',
                          'minor_version': 7
                      },
                      'release': {
                          'z_stream': None,
                          'major_version': 8,
                          'tag': None,
                          'os_name': 'RHEL',
                          'minor_version': 0
                      },
                      'in_packageset': {
                          'set_id': 20,
                          'package': [
                              {
                                  'name': 'espeak',
                                  'repository': 'rhel7-optional'
                              }
                          ]
                      },
                      'out_packageset': {  # can be None
                          'set_id': 21,
                          'package': [
                              {
                                  'name': 'espeak-ng',
                                  'repository': 'rhel8-AppStream'
                              }
                          ]
                      },
                      'architectures': [  # can be empty
                          'x86_64',
                          'aarch64',
                          'ppc64le',
                          's390x'
                      ]
                  }
    """

    event_id = entry.get('id') or 0

    action_id = entry['action']
    if action_id < 0 or action_id >= len(Action):
        raise ValueError('Found event with invalid action ID: {}'.format(action_id))
    action = Action(action_id)

    in_pkgs = parse_packageset(entry.get('in_packageset') or {})
    out_pkgs = parse_packageset(entry.get('out_packageset') or {})

    # parse_release handles missing release data, no need to supply a default value
    from_release = parse_release(entry.get('initial_release'))
    to_release = parse_release(entry.get('release'))

    architectures = entry.get('architectures') or []
    invalid_archs = tuple(arch for arch in architectures if arch not in architecture.ARCH_ACCEPTED)
    if invalid_archs:
        raise ValueError('Found event with invalid architecture{0}: {1}'.format('s' if len(invalid_archs) > 1 else '',
                                                                                ', '.join(invalid_archs)))

    # Parse modulestream maps
    modulestream_map_entry = entry.get('modulestream_maps') or []

    modulestream_maps = defaultdict(set)
    for mapping_entry in modulestream_map_entry:
        in_ms_entry = mapping_entry.get('in_modulestream', {})
        in_modulestream = (in_ms_entry.get('name'), in_ms_entry.get('stream')) if in_ms_entry else None

        out_ms_entry = mapping_entry.get('out_modulestream', {})
        out_modulestream = (out_ms_entry.get('name'), out_ms_entry.get('stream')) if out_ms_entry else None

        # One modulestream might have more than one output modulestream - eg. the output packages of split belong to
        # different modulestreams: (pkg_in, ms1) is split to (pkg_out1, ms2), (pkg_out2, ms3). Therefore, we need to
        # collect for every input modulestream all the output modulestreams it maps to.
        modulestream_maps[in_modulestream].add(out_modulestream)

    return event_by_modulestream_mapping(
        modulestream_maps,
        Event(event_id, action, in_pkgs, out_pkgs, from_release, to_release, architectures)
    )


def parse_packageset(packageset):
    """
    Get "input" or "output" packages and their repositories from each PES event.

    :return: set of Package tuples
    """
    packageset_pkgs = packageset.get('package', packageset.get('packages', []))
    parsed_pkgs = set()

    for package in packageset_pkgs:
        # The package can have either the modulestreams field (a list of modulestream entries)
        # or a modulestream entry containing a single module stream
        modulestream_entries = package.get('modulestreams')
        if not modulestream_entries:
            modulestream_entry = package.get('modulestream')
            if isinstance(modulestream_entry, dict):
                # There is only one module stream present
                modulestream_entries = (modulestream_entry,)

        if not modulestream_entries:
            modulestreams = (None, )
        else:
            modulestreams = tuple((ms['name'], ms['stream']) if ms else None for ms in modulestream_entries)

        pkg = Package(package['name'], package['repository'], modulestreams)
        parsed_pkgs.add(pkg)

    return parsed_pkgs


def parse_release(release):
    return (release['major_version'], release['minor_version']) if release else (0, 0)


# repos/system_upgrade/common/actors/peseventsscanner/libraries/pes_events_scanner.py

from collections import namedtuple
from functools import partial

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import peseventsscanner_repomap
from leapp.libraries.actor.pes_event_parsing import Action, get_pes_events, Package
from leapp.libraries.common.config import version
from leapp.libraries.stdlib import api
from leapp.libraries.stdlib.config import is_verbose
from leapp.models import (
    EnabledModules,
    InstalledRedHatSignedRPM,
    Module,
    PESIDRepositoryEntry,
    PESRpmTransactionTasks,
    RepositoriesBlacklisted,
    RepositoriesFacts,
    RepositoriesMapping,
    RepositoriesSetupTasks,
    RHUIInfo,
    RpmTransactionTasks
)

SKIPPED_PKGS_MSG = (
    'packages will be skipped because they are available only in '
    'target system repositories that are intentionally excluded from the '
    'list of repositories used during the upgrade. '
    'See the report message titled "Excluded target system repositories" '
    'for details.\nThe list of these packages:'
)


TransactionConfiguration = namedtuple('TransactionConfiguration', ('to_install', 'to_remove', 'to_keep'))


def get_cloud_provider_name(cloud_provider_variant):
    for cloud_provider_prefix in ('aws', 'azure', 'google'):
        if cloud_provider_variant.startswith(cloud_provider_prefix):
            return cloud_provider_prefix
    return cloud_provider_variant


def get_best_pesid_candidate(candidate_a, candidate_b, cloud_provider):
    cdn_candidate = None
    for candidate in (candidate_a, candidate_b):
        if candidate.rhui == cloud_provider:
            return candidate
        if not candidate.rhui:
            cdn_candidate = candidate

    # None of the candidate matches cloud provider and none of them is from CDN -
    # do not return anything as we don't want to get content from different cloud providers
    return cdn_candidate


def get_installed_pkgs():
    installed_pkgs = set()

    installed_rh_signed_rpm_msgs = api.consume(InstalledRedHatSignedRPM)
    installed_rh_signed_rpm_msg = next(installed_rh_signed_rpm_msgs, None)
    if list(installed_rh_signed_rpm_msgs):
        api.current_logger().warning('Unexpectedly received more than one InstalledRedHatSignedRPM message.')
    if not installed_rh_signed_rpm_msg:
        raise StopActorExecutionError('Cannot parse PES data properly due to missing list of installed packages',
                                      details={'Problem': 'Did not receive a message with installed Red Hat-signed '
                                                          'packages (InstalledRedHatSignedRPM)'})

    for pkg in installed_rh_signed_rpm_msg.items:
        modulestream = None
        if pkg.module and pkg.stream:
            modulestream = (pkg.module, pkg.stream)
        installed_pkgs.add(Package(name=pkg.name, repository=pkg.repository, modulestream=modulestream))

    return installed_pkgs


def get_transaction_configuration():
    """
    Get pkgs to install, keep and remove from the user configuration files in /etc/leapp/transaction/.

    These configuration files have higher priority than PES data.
    :return: RpmTransactionTasks model instance
    """
    transaction_configuration = TransactionConfiguration(to_install=[], to_remove=[], to_keep=[])

    _Pkg = partial(Package, repository=None, modulestream=None)

    for tasks in api.consume(RpmTransactionTasks):
        transaction_configuration.to_install.extend(_Pkg(name=pkg_name) for pkg_name in tasks.to_install)
        transaction_configuration.to_remove.extend(_Pkg(name=pkg_name) for pkg_name in tasks.to_remove)
        transaction_configuration.to_keep.extend(_Pkg(name=pkg_name) for pkg_name in tasks.to_keep)
    return transaction_configuration


def get_relevant_releases(events):
    """
    Get releases present in the PES Events that are relevant for this IPU.

    Relevant release happened between the source OS version and the target OS version.
    """
    # Collect releases that happened between source OS version and target OS version
    relevant_releases_match_list = [
        '> {0}'.format(api.current_actor().configuration.version.source),
        '<= {0}'.format(api.current_actor().configuration.version.target)
    ]
    releases = {event.to_release for event in events}
    releases = [r for r in releases if version.matches_version(relevant_releases_match_list, '{}.{}'.format(*r))]
    return sorted(releases)


def _get_enabled_modules():
    enabled_modules_msgs = api.consume(EnabledModules)
    enabled_modules_msg = next(enabled_modules_msgs, None)
    if list(enabled_modules_msgs):
        api.current_logger().warning('Unexpectedly received more than one EnabledModules message.')
    if not enabled_modules_msg:
        raise StopActorExecutionError('Cannot parse PES data properly due to missing list of enabled modules',
                                      details={'Problem': 'Did not receive a message with enabled module '
                                                          'streams (EnabledModules)'})
    return enabled_modules_msg.modules


def compute_pkg_changes_between_consequent_releases(source_installed_pkgs,
                                                    events,
                                                    release,
                                                    seen_pkgs,
                                                    pkgs_to_demodularize):
    # Start with the installed packages and modify the set according to release events
    target_pkgs = set(source_installed_pkgs)

    release_events = [e for e in events if e.to_release == release]

    for event in release_events:
        # PRESENCE events have a different semantics than the other events - they add a package to a target state
        # only if it had been seen (installed) during the course of the overall target packages
        if event.action == Action.PRESENT:
            for pkg in event.in_pkgs:
                if pkg in seen_pkgs:
                    if pkg in target_pkgs:
                        # Remove the package with the old repository, add the one with the new one
                        target_pkgs.remove(pkg)
                    target_pkgs.add(pkg)
        elif event.action == Action.DEPRECATED:
            if event.in_pkgs.intersection(source_installed_pkgs):
                # Remove packages with old repositories add packages with the new one
                target_pkgs = target_pkgs.difference(event.in_pkgs)
                target_pkgs = target_pkgs.union(event.in_pkgs)
        else:
            # All other packages have the same semantics - they remove their in_pkgs from the system with given
            # from_release and add out_pkgs to the system matching to_release
            are_all_in_pkgs_present = all(in_pkg in source_installed_pkgs for in_pkg in event.in_pkgs)
            is_any_in_pkg_present = any(in_pkg in source_installed_pkgs for in_pkg in event.in_pkgs)

            # For MERGE to be relevant it is sufficient for only one of its in_pkgs to be installed
            if are_all_in_pkgs_present or (event.action == Action.MERGED and is_any_in_pkg_present):
                # In pkgs are present, event can be applied
                target_pkgs = target_pkgs.difference(event.in_pkgs)
                target_pkgs = target_pkgs.union(event.out_pkgs)

        pkgs_to_demodularize = pkgs_to_demodularize.difference(event.in_pkgs)

    return (target_pkgs, pkgs_to_demodularize)


def compute_packages_on_target_system(source_pkgs, events, releases):

    seen_pkgs = set(source_pkgs)  # Used to track whether PRESENCE events can be applied
    target_pkgs = set(source_pkgs)

    source_major_version = int(version.get_source_major_version())
    did_processing_cross_major_version = False
    pkgs_to_demodularize = set()  # Modified by compute_pkg_changes

    for release in releases:
        if not did_processing_cross_major_version and release[0] > source_major_version:
            did_processing_cross_major_version = True
            pkgs_to_demodularize = {pkg for pkg in target_pkgs if pkg.modulestream}

        target_pkgs, pkgs_to_demodularize = compute_pkg_changes_between_consequent_releases(target_pkgs, events,
                                                                                            release, seen_pkgs,
                                                                                            pkgs_to_demodularize)
        seen_pkgs = seen_pkgs.union(target_pkgs)

    demodularized_pkgs = {Package(pkg.name, pkg.repository, None) for pkg in pkgs_to_demodularize}
    demodularized_target_pkgs = target_pkgs.difference(pkgs_to_demodularize).union(demodularized_pkgs)

    return (demodularized_target_pkgs, pkgs_to_demodularize)


def compute_rpm_tasks_from_pkg_set_diff(source_pkgs, target_pkgs, pkgs_to_demodularize):
    source_state_pkg_names = {pkg.name for pkg in source_pkgs}
    target_state_pkg_names = {pkg.name for pkg in target_pkgs}

    pkgs_to_install = sorted(target_state_pkg_names.difference(source_state_pkg_names))
    pkgs_to_remove = sorted(source_state_pkg_names.difference(target_state_pkg_names))

    if pkgs_to_install or pkgs_to_remove:
        # NOTE(mhecko): Here we do not want to consider any package that does not have a reference in PES data. There
        # might be missing modularity information, and although the algorithm is correct, trying to enable
        # a non-existent modulestream due to missing modulestream information results in a crash.
        target_pkgs_without_demodularized_pkgs = target_pkgs.difference(pkgs_to_demodularize)

        # Collect the enabled modules as tuples in a set, so we produce every module to reset exactly once
        enabled_modules = {(module.name, module.stream) for module in _get_enabled_modules()}
        modules_to_reset = [Module(name=ms[0], stream=ms[1]) for ms in enabled_modules]

        target_modulestreams = {pkg.modulestream for pkg in target_pkgs_without_demodularized_pkgs if pkg.modulestream}
        modules_to_enable = [Module(name=ms[0], stream=ms[1]) for ms in target_modulestreams]

        return PESRpmTransactionTasks(to_install=pkgs_to_install,
                                      to_remove=pkgs_to_remove,
                                      modules_to_enable=modules_to_enable,
                                      modules_to_reset=modules_to_reset)
    return None


def report_skipped_packages(title, message, skipped_pkgs, remediation=None):
    skipped_pkgs = sorted(skipped_pkgs)

    def make_summary_entry_for_skipped_pkg(pkg):
        entry_template = '- {name}{modulestream} (repoid: {repository})'
        modulestream_str = '' if not pkg.modulestream else '[{}:{}]'.format(*pkg.modulestream)
        return entry_template.format(name=pkg.name, modulestream=modulestream_str, repository=pkg.repository)

    summary = '{} {}\n{}'.format(len(skipped_pkgs),
                                 message,
                                 '\n'.join(make_summary_entry_for_skipped_pkg(pkg) for pkg in skipped_pkgs))
    report_content = [
        reporting.Title(title),
        reporting.Summary(summary),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Groups([reporting.Groups.REPOSITORY]),
    ]
    if remediation:
        report_content += [reporting.Remediation(hint=remediation)]
    report_content += [reporting.RelatedResource('package', p[0]) for p in skipped_pkgs]
    reporting.create_report(report_content)
    if is_verbose():
        api.current_logger().info(summary)


def remove_new_packages_from_blacklisted_repos(source_pkgs, target_pkgs):
    """
    Remove newly installed packages from blacklisted repositories that were computed to be on the target system.
    """
    blacklisted_repoids = get_blacklisted_repoids()
    new_pkgs = target_pkgs.difference(source_pkgs)
    pkgs_from_blacklisted_repos = set(pkg for pkg in new_pkgs if pkg.repository in blacklisted_repoids)

    if pkgs_from_blacklisted_repos:
        report_skipped_packages(
            title='Packages available in excluded repositories will not be installed',
            message=SKIPPED_PKGS_MSG,
            skipped_pkgs=pkgs_from_blacklisted_repos,
        )
    return blacklisted_repoids, target_pkgs.difference(pkgs_from_blacklisted_repos)


def get_blacklisted_repoids():
    repos_blacklisted = set()
    for blacklist in api.consume(RepositoriesBlacklisted):
        repos_blacklisted.update(blacklist.repoids)
    return repos_blacklisted


def get_enabled_repoids():
    """
    Collects repoids of all enabled repositories on the source system.

    :param repositories_facts: Iterable of RepositoriesFacts containing repositories related info about source system.
    :return: Set of all enabled repository IDs present on the source system.
    :rtype: Set[str]
    """
    enabled_repoids = set()
    for repos in api.consume(RepositoriesFacts):
        for repo_file in repos.repositories:
            for repo in repo_file.data:
                if repo.enabled:
                    enabled_repoids.add(repo.repoid)
    return enabled_repoids


def get_pesid_to_repoid_map(target_pesids):
    """
    Get a dictionary mapping all PESID repositories to their corresponding repoid.

    :param target_pesids: The set of target PES IDs needed to be mapped
    :return: Dictionary mapping the target_pesids to their corresponding repoid
    """

    repositories_map_msgs = api.consume(RepositoriesMapping)
    repositories_map_msg = next(repositories_map_msgs, None)
    if list(repositories_map_msgs):
        api.current_logger().warning('Unexpectedly received more than one RepositoriesMapping message.')
    if not repositories_map_msg:
        raise StopActorExecutionError(
            'Cannot parse RepositoriesMapping data properly',
            details={'Problem': 'Did not receive a message with mapped repositories'}
        )

    rhui_info = next(api.consume(RHUIInfo), RHUIInfo(provider=''))

    repomap = peseventsscanner_repomap.RepoMapDataHandler(repositories_map_msg, cloud_provider=rhui_info.provider)

    # NOTE: We have to calculate expected target repositories like in the setuptargetrepos actor.
    # It's planned to handle this in different a way in future...

    enabled_repoids = get_enabled_repoids()
    default_channels = peseventsscanner_repomap.get_default_repository_channels(repomap, enabled_repoids)
    repomap.set_default_channels(default_channels)

    exp_pesid_repos = repomap.get_expected_target_pesid_repos(enabled_repoids)
    # FIXME: this is hack now. In case some packages will need a repository
    # with pesid that is not mapped by default regarding the enabled repos,
    # let's use this one representative repository (baseos/appstream) to get
    # data for a guess of the best repository from the requires target pesid..
    # FIXME: this could now fail in case all repos are disabled...
    representative_repo = exp_pesid_repos.get(
        peseventsscanner_repomap.DEFAULT_PESID[version.get_target_major_version()], None
    )
    if not representative_repo:
        api.current_logger().warning('Cannot determine the representative target base repository.')
        api.current_logger().info(
            'Fallback: Create an artificial representative PESIDRepositoryEntry for the repository mapping'
        )
        representative_repo = PESIDRepositoryEntry(
            pesid=peseventsscanner_repomap.DEFAULT_PESID[version.get_target_major_version()],
            arch=api.current_actor().configuration.architecture,
            major_version=version.get_target_major_version(),
            repoid='artificial-repoid',
            repo_type='rpm',
            channel='ga',
            rhui='',
        )

    for pesid in target_pesids:
        if pesid in exp_pesid_repos:
            continue
        # some packages are moved to repos outside of default repomapping
        # try to find the best possible repo for them..
        # FIXME: HACK NOW
        # good way is to modify class to search repo with specific criteria..
        if not representative_repo:
            api.current_logger().warning('Cannot find suitable repository for PES ID: {}'.format(pesid))
            continue
        pesid_repo = repomap._find_repository_target_equivalent(representative_repo, pesid)

        if not pesid_repo:
            api.current_logger().warning('Cannot find suitable repository for PES ID: {}'.format(pesid))
            continue
        exp_pesid_repos[pesid] = pesid_repo

    # Map just pesids to their corresponding repoids ({to_pesid: repoid})
    repositories_mapping = {}
    for pesid, repository in exp_pesid_repos.items():
        if pesid not in target_pesids:
            # We can skip this repo as it was not identified as needed during the processing of PES events
            continue
        if not repository:
            # TODO
            continue
        repositories_mapping[pesid] = repository.repoid

    return repositories_mapping


def replace_pesids_with_repoids_in_packages(packages, source_pkgs_repoids):
    """Replace packages with PESID in their .repository field with ones that have repoid providing the package."""
    # We want to map only PESIDs - if some package had no events, it will its repository set to source system repoid
    packages_with_pesid = {pkg for pkg in packages if pkg.repository not in source_pkgs_repoids}
    packages_without_pesid = packages.difference(packages_with_pesid)

    required_target_pesids = {pkg.repository for pkg in packages_with_pesid}

    pesid_to_repoid_map = get_pesid_to_repoid_map(required_target_pesids)

    packages_without_known_repoid = {pkg for pkg in packages_with_pesid if pkg.repository not in pesid_to_repoid_map}

    if packages_without_known_repoid:
        report_skipped_packages(
            title='Packages from unknown repositories may not be installed',
            message='packages may not be installed or upgraded due to repositories unknown to leapp:',
            skipped_pkgs=packages_without_known_repoid,
            remediation=(
                'Please file a bug in http://bugzilla.redhat.com/ for leapp-repository component of '
                'the Red Hat Enterprise Linux product.'
            ),
        )

    packages_with_known_repoid = packages_with_pesid.difference(packages_without_known_repoid)
    packages_with_repoid = {
        Package(p.name, pesid_to_repoid_map[p.repository], p.modulestream) for p in packages_with_known_repoid
    }
    # Packages without pesid are those for which we do not have an event, keep them in target packages
    return packages_with_repoid.union(packages_without_pesid)


def apply_transaction_configuration(source_pkgs):
    source_pkgs_with_conf_applied = set(source_pkgs)
    transaction_configuration = get_transaction_configuration()

    source_pkgs_with_conf_applied = source_pkgs.union(transaction_configuration.to_install)

    # Transaction configuration contains only names of packages to install/remove/keep - there is no modularity
    # information - modify target_pkgs in a way ignoring modulestream information
    pkg_name_to_pkg_info_map = {pkg.name: pkg for pkg in source_pkgs}

    for pkg in transaction_configuration.to_remove:
        if pkg.name in pkg_name_to_pkg_info_map:
            source_pkgs_with_conf_applied.remove(pkg_name_to_pkg_info_map[pkg.name])

    for pkg in transaction_configuration.to_keep:
        if pkg.name in pkg_name_to_pkg_info_map:
            source_pkgs_with_conf_applied.add(pkg_name_to_pkg_info_map[pkg.name])

    return source_pkgs_with_conf_applied


def process():
    # Retrieve data - installed_pkgs, transaction configuration, pes events
    events = get_pes_events('/etc/leapp/files', 'pes-events.json')
    releases = get_relevant_releases(events)
    source_pkgs = get_installed_pkgs()
    source_pkgs = apply_transaction_configuration(source_pkgs)

    # Keep track of what repoids have the source packages to be able to determine what are the PESIDs of the computed
    # packages of the target system, so we can distinguish what needs to be repomapped
    repoids_of_source_pkgs = {pkg.repository for pkg in source_pkgs}

    # Apply events - compute what packages should the target system have
    target_pkgs, pkgs_to_demodularize = compute_packages_on_target_system(source_pkgs, events, releases)

    # Packages coming out of the events have PESID as their repository, however, we need real repoid
    target_pkgs = replace_pesids_with_repoids_in_packages(target_pkgs, repoids_of_source_pkgs)

    # Apply the desired repository blacklisting
    blacklisted_repoids, target_pkgs = remove_new_packages_from_blacklisted_repos(source_pkgs, target_pkgs)

    # Look at the target packages and determine what repositories to enable
    target_repoids = sorted(set(p.repository for p in target_pkgs) - blacklisted_repoids - repoids_of_source_pkgs)
    repos_to_enable = RepositoriesSetupTasks(to_enable=target_repoids)
    api.produce(repos_to_enable)

    # Compare the packages on source system and the computed packages on target system and determine what to install
    rpm_tasks = compute_rpm_tasks_from_pkg_set_diff(source_pkgs, target_pkgs, pkgs_to_demodularize)
    if rpm_tasks:
        api.produce(rpm_tasks)


# repos/system_upgrade/common/actors/peseventsscanner/libraries/peseventsscanner_repomap.py

from leapp.libraries.common.config import get_target_product_channel
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
from leapp.libraries.stdlib import api

DEFAULT_PESID = {
    '7': 'rhel7-base',
    '8': 'rhel8-BaseOS',
    '9': 'rhel9-BaseOS'
}


def _get_channel_prio(pesid_repo):
    priorities = {
        'beta': 0,
        'ga': 1,
    }
    return priorities.get(pesid_repo.channel, 10)


class RepoMapDataHandler(object):
    """
    Provide the basic functionality to work with the repository data easily.
    """

    def __init__(self, repo_map, cloud_provider='', default_channels=None):
        """
        Initialize the object based on the given RepositoriesMapping msg.

        Expects that msg contains just stuff related for the current IPU
        (at least mapping and repos for the used upgrade path and architecture).

        :param repo_map: A valid RepositoryMapping message.
        :type repo_map: RepositoryMapping
        :param default_channels: A list of default channels to use when a target repository
                                 equivalent exactly matching a source repository was not found.
        :type default_channels: List[str]
        :param prio_channel: Prefer repositories with this channel when looking for target equivalents.
        :type prio_channel: str
        """
        # NOTE: currently I am keeping this default data structure that is not
        # ideal for work, but there is not any significant impact..
        self.repositories = repo_map.repositories
        self.mapping = repo_map.mapping
        # FIXME(pstodulk): what about default_channel -> fallback_channel
        # hardcoded always as ga? instead of list of channels..
        # it'd be possibly confusing naming now...
        self.default_channels = default_channels or ['ga']

        # Make self.prio_channel None if the user did not specify any target channels, so that self.default_channels
        # will be used instead
        self.prio_channel = get_target_product_channel(default=None)

        self.cloud_provider = cloud_provider

        # Cloud provider might have multiple variants, e.g, aws: (aws, aws-sap-es4) - normalize it
        cloud_providers = ('aws', 'azure', 'google')
        for provider in cloud_providers:
            if cloud_provider.startswith(provider):
                self.cloud_provider = provider
                break

    def set_default_channels(self, default_channels):
        """
        Set the default channels that are used as a fallback when searching
        for the right target repository.

        Usually it's not problem to find a target repository that matches
        the source repository, however in some cases the target repository
        doesn't have to be available in the required (premium) channel but
        could be present in the standard one.

        This is used usually for fallbacks and it's prerequisite for the time
        the required channel will not be present for the particular repository.
        E.g. can happen for layered products which has different lifecycles
        and doesn't have to provide a special premium channels at all. E.g.
        the Extras repository has GA and Beta channels, but no EUS. In case the
        EUS is required, returns the GA one instead when this is present.

        It's recommended to make the GA ('ga') always present in the default
        list.

        :param default_channels: Default channels to use when a target equivalent to a source repository that
                                 matches its target channel properties exactly could not be found.
        :type default_channels: List[str]
        """
        self.default_channels = default_channels

    def get_pesid_repo_entry(self, repoid, major_version):
        """
        Retrieve the PESIDRepositoryEntry that matches the given repoid and OS major version.

        If multiple pesid repo entries with the same repoid were found, the entry with rhui matching the source
        system's rhui info will be returned. If no entry with matching rhui exists, the CDN one is returned if any.

        :param repoid: RepoID that should the PESIDRepositoryEntry match.
        :type repoid: str
        :param major_version: RepoID that should the PESIDRepositoryEntry match.
        :type major_version: str
        :return: The PESIDRepositoryEntry matching the given repoid and major_version or None if no such
                 entry could be found.
        :rtype: Optional[PESIDRepositoryEntry]
        """
        matching_pesid_repos = []
        for pesid_repo in self.repositories:
            if pesid_repo.repoid == repoid and pesid_repo.major_version == major_version:
                matching_pesid_repos.append(pesid_repo)

        if len(matching_pesid_repos) == 1:
            # Perform no heuristics if only a single pesid repository with matching repoid found
            return matching_pesid_repos[0]

        # Multiple (different) repositories with the same repoid found (can happen in clouds) - prefer the cloud one
        cdn_pesid_repo = None
        for pesid_repo in matching_pesid_repos:
            if pesid_repo.rhui == self.cloud_provider:
                return pesid_repo
            if not pesid_repo.rhui:
                cdn_pesid_repo = pesid_repo

        # If we did not find a repoid for the current cloud provider, return the CDN repository
        return cdn_pesid_repo  # might be None e.g. if we are on Azure with an AWS repository enabled (unlikely)

    def get_target_pesids(self, source_pesid):
        """
        Return sorted list of target PES IDs for the given source PES ID.

        :param source_pesid: Source PES ID to find equivalents for.
        :type source_pesid: src
        :return: The list of target PES IDs the provided source_pesid is mapped to.
        :rtype: List[PESIDRepositoryEntry]
        """
        pesids = set()
        for repomap in self.mapping:
            if repomap.source == source_pesid:
                pesids.update(repomap.target)
        return sorted(pesids)

    def get_pesid_repos(self, pesid, major_version):
        """
        Get the list of PESIDRepositoryEntry with the specified PES ID and OS major version.

        :param pesid: PES ID of the repositories to be retrieved.
        :type pesid: str
        :param major_version: OS major version of the repositories to be retrieved.
        :type major_version: str
        :return: A list of PESIDRepositoryEntries that match the provided PES ID and OS major version.
        :rtype: List[PESIDRepositoryEntry]
        """
        pesid_repos = []
        for pesid_repo in self.repositories:
            if pesid_repo.pesid == pesid and pesid_repo.major_version == major_version:
                pesid_repos.append(pesid_repo)
        return pesid_repos

    def get_source_pesid_repos(self, pesid):
        """
        Return the list of PESIDRepositoryEntry objects for a specified PES ID
        matching the source OS major version.

        :param pesid: The PES ID for which to retrieve PESIDRepositoryEntries.
        :type pesid: str
        :return: A list of PESIDRepositoryEntries that match the provided PES ID and have
                 the OS Major version same as the source OS.
        :rtype: List[PESIDRepositoryEntry]
        """
        return self.get_pesid_repos(pesid, get_source_major_version())

    def get_target_pesid_repos(self, pesid):
        """
        Return the list of PESIDRepositoryEntry objects for a specified PES ID
        matching the target OS major version.

        :param pesid: The PES ID for which to retrieve PESIDRepositoryEntries.
        :type pesid: str
        :return: A list of PESIDRepositoryEntries that match the provided PES ID and have
                 the OS Major version same as the target OS.
        :rtype: List[PESIDRepositoryEntry]
        """
        return self.get_pesid_repos(pesid, get_target_major_version())

    def _find_repository_target_equivalent(self, src_pesidrepo, target_pesid):
        """
        Find the target repository that is the best-match to the source one with the given
        target PES ID.

        :param src_pesidrepo: The source repository to find equivalent to.
        :type src_pesidrepo: PESIDRepositoryEntry
        :param target_pesid: The target PES ID which the target repository must contain.
        :type target_pesid: str
        :return: A target equivalent of given repository.
        :rtype: Optional[PESIDRepositoryEntry]
        """

        candidates = []
        for candidate in self.get_target_pesid_repos(target_pesid):
            matches_rhui = candidate.rhui == src_pesidrepo.rhui
            matches_repo_type = candidate.repo_type == 'rpm'
            matches_arch = candidate.arch == api.current_actor().configuration.architecture

            if matches_rhui and matches_arch and matches_repo_type:
                # user can specify in future the specific channel should be
                # prioritized always (e.g. want to go to EUS...).
                channel = self.prio_channel or src_pesidrepo.channel
                if candidate.channel == channel:
                    return candidate
                candidates.append(candidate)

        # Fallback...
        # Could not find exact-match, so go through candidates if we find an
        # alternative in one of default channels (usually just 'ga')
        for channel in self.default_channels:
            for candidate in candidates:
                if channel == candidate.channel:
                    return candidate

        # This is a case, that must be handled by the caller
        return None

    def get_mapped_target_pesid_repos(self, src_pesidrepo):
        """
        Returns the dict of form {target_pesid: target PESIDRepositoryEntry} containing
        pesids of repositories that is the source pesidrepo mapped to as keys and with
        the best fitting repository for each of the target pesids as values.

        The function always returns the best candidate for every target pesid
        that fits to the given source repository. In case no repository is find
        for a pesid, the value in dict is None: {pesid: None}

        :param src_pesidrepo: The PESIDRepositoryEntry to find the target pesids and the corresponding
                              best-fitting repositories to.
        :type src_pesidrepo: PESIDRepositoryEntry
        :return: A dictionary of the form described above.
        :rtype: Dict[str, PESIDRepositoryEntry]
        """
        result = {}
        for target_pesid in self.get_target_pesids(src_pesidrepo.pesid):
            result[target_pesid] = self._find_repository_target_equivalent(src_pesidrepo, target_pesid)
        return result

    def get_mapped_target_repoids(self, src_pesidrepo):
        """
        Return the list of target repoids for the given src_pesidrepo.

        Some actors do not has to check whether a target repository has been
        found for each target PES ID and details about the target repositories.
        For such actors it's ok to see just list of repoids they should be
        interested about and keep the proper checks for the right actor.

        :param src_pesidrepo: The PESIDRepositoryEntry to find the corresponding best-fitting repositories to.
        :type src_pesidrepo: PESIDRepositoryEntry
        :return: A dictionary of the form described above.
        :rtype: Dict[str, PESIDRepositoryEntry]
        """
        return [repo.repoid for repo in self.get_mapped_target_pesid_repos(src_pesidrepo).values() if repo]

    def get_expected_target_pesid_repos(self, src_repoids):
        """
        Return {target_pesid: PESIDRepositoryEntry} with expected target repositories.

        If some repositories are mapped to a target pesid for which no equivalent
        repository is discovered, such a key contains just None value.

        :param src_repoids: list of present source repoids that should be mapped to target repositories
        :type src_repoids: List[str]
        :rtype: {str: PESIDRepositoryEntry}
        """
        # {pesid: target_repo}
        target_repos_best_candidates = {}
        for src_repoid in src_repoids:
            src_pesidrepo = self.get_pesid_repo_entry(src_repoid, get_source_major_version())
            if not src_pesidrepo:
                # unmapped or custom repo -> skip this one
                continue

            for target_pesid, target_candidate in self.get_mapped_target_pesid_repos(src_pesidrepo).items():
                best_candidate = target_repos_best_candidates.get(target_pesid, None)
                if not best_candidate:
                    # we need to initialize the pesid even when the target_candidate is empty
                    # to know we possibly miss something
                    target_repos_best_candidates[target_pesid] = target_candidate
                if not target_candidate:
                    # It's not crucial in this moment - the pesid can be still filled
                    # by other maps. However log the warning as it is still unexpected
                    # with the valid repomap data
                    api.current_logger().warning(
                        'Cannot find any mapped target repository from the'
                        ' {pesid} family for the {repoid} repository.'
                        .format(repoid=src_repoid, pesid=target_pesid)
                    )
                    continue

                if best_candidate and _get_channel_prio(target_candidate) > _get_channel_prio(best_candidate):
                    # NOTE(pstodulk): we want just one target repository from the "PESID family"
                    # priority: beta < ga < all_else
                    # Why all_else?
                    # -> we do not expect multiple different premium channels present on the system
                    target_repos_best_candidates[target_pesid] = target_candidate
        return target_repos_best_candidates


def get_default_repository_channels(repomap, src_repoids):
    """
    Returns the default repository channels. The 'ga' channel is always included and is the last
    one in the list, so it is the lowest priority when checking for channels.

    :param repomap: A RepoMapDataHandler instance containing the RepositoriesMapping data.
    :type repomap: RepoMapDataHandler
    :param src_repoids: Repositories present on the source system.
    :type src_repoids: List[str]
    :rtype: List[str]
    """
    default_pesid = DEFAULT_PESID[get_source_major_version()]
    top_prio_pesid_repo = None
    for repoid in src_repoids:
        pesid_repo = repomap.get_pesid_repo_entry(repoid, get_source_major_version())
        if not pesid_repo or pesid_repo.pesid != default_pesid:
            continue
        if not top_prio_pesid_repo or _get_channel_prio(pesid_repo) > _get_channel_prio(top_prio_pesid_repo):
            top_prio_pesid_repo = pesid_repo

    # always return at least 'ga'
    if not top_prio_pesid_repo or top_prio_pesid_repo.channel == 'ga':
        return ['ga']

    # keep this order to prefer higher prio to check first
    return [top_prio_pesid_repo.channel, 'ga']


# repos/system_upgrade/common/actors/peseventsscanner/tests/test_event_parsing.py

import os.path
from collections import namedtuple

import pytest

from leapp.libraries.actor.pes_event_parsing import (
    Action,
    Event,
    Package,
    parse_entry,
    parse_packageset,
    parse_pes_events
)

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


def test_parse_packageset(current_actor_context):
    pkgset = {'package': [{'name': 'pkg1', 'repository': 'Repo'}]}

    parsed = parse_packageset(pkgset)
    assert len(parsed) == 1
    assert Package('pkg1', 'Repo', (None,)) in parsed

    assert parse_packageset({}) == set()
    assert parse_packageset({'set_id': 0}) == set()


def test_parse_packageset_modular(current_actor_context):
    modulestreams = [{'name': 'hey', 'stream': 'lol1'}, {'name': 'hey', 'stream': 'lol2'}]
    pkgset = {'package': [{'name': 'pkg1', 'repository': 'Repo', 'modulestreams': [None]},
                          {'name': 'pkg2', 'repository': 'Repo', 'modulestreams': modulestreams}]}

    parsed = parse_packageset(pkgset)
    assert len(parsed) == 2
    assert Package('pkg1', 'Repo', (None,)) in parsed
    assert Package('pkg2', 'Repo', (('hey', 'lol1'), ('hey', 'lol2'))) in parsed

    assert parse_packageset({}) == set()
    assert parse_packageset({'set_id': 0}) == set()


PARSE_ENTRY_INPUTS = [
    (
        # Input
        {
            'action': 4,
            'architectures': ['x86_64', 's390x'],
            'in_packageset': {
                'package': [{'name': 'original', 'repository': 'repo'}]
            },
            'out_packageset': {
                'package': [{'name': 'split01', 'repository': 'repo'}, {'name': 'split02', 'repository': 'repo'}]
            }
        },
        # Expected output
        [
            Event(action=Action.SPLIT, architectures=['x86_64', 's390x'], from_release=(0, 0), to_release=(0, 0), id=0,
                  in_pkgs={Package('original', 'repo', None)},
                  out_pkgs={Package('split01', 'repo', None), Package('split02', 'repo', None)})
        ]
    ),
    (
        # Input
        {
            'action': 1,
            'in_packageset': {
                'package': [{'name': 'removed', 'repository': 'repo'}]
            }
        },
        # Expected output
        [
            Event(action=Action.REMOVED, architectures=[], from_release=(0, 0), to_release=(0, 0), id=0,
                  in_pkgs={Package('removed', 'repo', None)},
                  out_pkgs=set())
        ]
    ),
    ({'action': 10}, ValueError),  # Invalid action
    ({'action': -1}, ValueError),  # Invalid action
    (
        {'action': 1, 'architectures': ['ia-64']},  # Invalid architecture
        ValueError
    ),
]


@pytest.mark.parametrize(('pes_entry_data', 'expected_output'), PARSE_ENTRY_INPUTS)
def test_parse_entry(current_actor_context, pes_entry_data, expected_output):
    """
    Tests whether the PES event is correctly parsed from the supplied dictionary with the same
    structure as are the data stored inside the json.
    """

    if isinstance(expected_output, list):
        events = parse_entry(pes_entry_data)
        assert len(events) == len(expected_output)
        for event in events:
            assert event in expected_output
    else:
        with pytest.raises(expected_output):
            parse_entry(pes_entry_data)


def test_parse_pes_events(current_actor_context):
    """
    Tests whether all events are correctly parsed from the provided string with the JSON data.
    """
    with open(os.path.join(CUR_DIR, 'files/sample01.json')) as f:
        events = parse_pes_events(f.read())
    assert len(events) == 2
    assert events[0].action == Action.SPLIT
    assert events[0].in_pkgs == {Package('original', 'repo', None)}
    assert events[0].out_pkgs == {Package('split01', 'repo', None), Package('split02', 'repo', None)}
    assert events[1].action == Action.REMOVED
    assert events[1].in_pkgs == {Package('removed', 'repo', None)}
    assert events[1].out_pkgs == set()


def test_parse_pes_events_with_modulestreams(current_actor_context):
    """
    Tests whether all events are correctly parsed from the provided string with the JSON data.
    """
    with open(os.path.join(CUR_DIR, 'files/sample04.json')) as f:
        events = parse_pes_events(f.read())
    assert len(events) == 5
    Expected = namedtuple('Expected', 'action,in_pkgs,out_pkgs')
    expected = [
        Expected(action=Action.SPLIT, in_pkgs={Package('original', 'repo', ('module', 'stream_in'))}, out_pkgs={
                 Package('split01', 'repo', None), Package('split02', 'repo', None)}),
        Expected(action=Action.SPLIT, in_pkgs={Package('original', 'repo', None)},
                 out_pkgs={Package('split01', 'repo', ('module', 'stream_out')),
                           Package('split02', 'repo', ('module', 'stream_out'))}),
        Expected(action=Action.REMOVED, in_pkgs={Package('removed', 'repo', None)}, out_pkgs=set()),
        Expected(action=Action.RENAMED, in_pkgs={Package('modularized', 'repo', ('module', 'stream_in'))}, out_pkgs={
                 Package('demodularized', 'repo', None)}),
        Expected(action=Action.RENAMED, in_pkgs={Package('modularized', 'repo', None)}, out_pkgs={
                 Package('demodularized', 'repo', None)}),
    ]

    for event in events:
        for idx, expectation in enumerate(list(expected)):
            if expectation.action == event.action and expectation.in_pkgs == event.in_pkgs:
                assert event.out_pkgs == expectation.out_pkgs
                expected.pop(idx)
                break
        if not expected:
            break
    assert not expected


# repos/system_upgrade/common/actors/peseventsscanner/tests/test_pes_event_scanner.py

from functools import partial

import pytest

from leapp.libraries.actor import pes_events_scanner
from leapp.libraries.actor.pes_event_parsing import Event
from leapp.libraries.actor.pes_events_scanner import (
    Action,
    api,
    compute_packages_on_target_system,
    compute_rpm_tasks_from_pkg_set_diff,
    get_installed_pkgs,
    Package,
    process,
    reporting,
    TransactionConfiguration
)
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, produce_mocked
from leapp.models import (
    EnabledModules,
    InstalledRedHatSignedRPM,
    PESIDRepositoryEntry,
    PESRpmTransactionTasks,
    RepoMapEntry,
    RepositoriesFacts,
    RepositoriesMapping,
    RepositoriesSetupTasks,
    RepositoryData,
    RepositoryFile,
    RHUIInfo,
    RPM
)


def pkgs_into_tuples(pkgs):
    return {(p.name, p.repository, p.modulestream) for p in pkgs}


@pytest.mark.parametrize(
    ('installed_pkgs', 'events', 'releases', 'expected_target_pkgs'),
    (
        (
            {Package('original', 'rhel7-repo', None)},
            [
                Event(1, Action.SPLIT, {Package('original', 'rhel7-repo', None)},
                      {Package('split01', 'rhel8-repo', None), Package('split02', 'rhel8-repo', None)},
                      (7, 6), (8, 0), [])
            ],
            [(8, 0)],
            {Package('split01', 'rhel8-repo', None), Package('split02', 'rhel8-repo', None), }
        ),
        (
            {Package('removed', 'rhel7-repo', None)},
            [Event(1, Action.REMOVED, {Package('removed', 'rhel7-repo', None)}, set(), (7, 6), (8, 0), [])],
            [(8, 0)],
            set()
        ),
        (
            {Package('present', 'rhel7-repo', None)},
            [Event(1, Action.PRESENT, {Package('present', 'rhel8-repo', None)}, set(), (7, 6), (8, 0), [])],
            [(8, 0)],
            {Package('present', 'rhel8-repo', None)}
        ),
        (
            {Package('reintroduced', 'rhel7-repo', None)},
            [
                Event(1, Action.REMOVED, {Package('reintroduced', 'rhel8-repo', None)}, set(), (7, 6), (8, 0), []),
                Event(2, Action.PRESENT, {Package('reintroduced', 'rhel8-repo', None)}, set(), (8, 0), (8, 1), []),
            ],
            [(8, 0), (8, 1)],
            {Package('reintroduced', 'rhel8-repo', None)}
        ),
        (
            {Package('merge-in1', 'rhel7-repo', None), Package('merge-in2', 'rhel7-repo', None)},
            [
                Event(1, Action.MERGED,
                      {Package('merge-in1', 'rhel7-repo', None), Package('merge-in2', 'rhel7-repo', None)},
                      {Package('merge-out', 'rhel8-repo', None)}, (7, 6), (8, 0), []),
            ],
            [(8, 0)],
            {Package('merge-out', 'rhel8-repo', None)}
        ),
        (
            {Package('merge-in1', 'rhel7-repo', None)},
            [
                Event(1, Action.MERGED,
                      {Package('merge-in1', 'rhel7-repo', None), Package('merge-in2', 'rhel7-repo', None)},
                      {Package('merge-out', 'rhel8-repo', None)}, (7, 6), (8, 0), []),
            ],
            [(8, 0)],
            {Package('merge-out', 'rhel8-repo', None)}
        ),
        (
            {Package('deprecated', 'rhel7-repo', None)},
            [Event(1, Action.DEPRECATED, {Package('deprecated', 'rhel8-repo', None)}, set(), (7, 6), (8, 0), [])],
            [(8, 0)],
            {Package('deprecated', 'rhel8-repo', None)}
        ),
        (
            {Package('replaced-in', 'rhel7-repo', None)},
            [
                Event(1, Action.REPLACED, {Package('replaced-in', 'rhel7-repo', None)},
                      {Package('replaced-out', 'rhel8-repo', None)}, (7, 6), (8, 0), [])
            ],
            [(8, 0)],
            {Package('replaced-out', 'rhel8-repo', None)}
        ),
        (
            {Package('moved-in', 'rhel7-repo', None)},
            [
                Event(1, Action.MOVED, {Package('moved-in', 'rhel7-repo', None)},
                      {Package('moved-out', 'rhel8-repo', None)}, (7, 6), (8, 0), [])
            ],
            [(8, 0)],
            {Package('moved-out', 'rhel8-repo', None)}
        ),
        (
            {Package('renamed-in', 'rhel7-repo', None)},
            [
                Event(1, Action.RENAMED, {Package('renamed-in', 'rhel7-repo', None)},
                      {Package('renamed-out', 'rhel8-repo', None)}, (7, 6), (8, 0), [])
            ],
            [(8, 0)],
            {Package('renamed-out', 'rhel8-repo', None)}
        ),
    )
)
def test_event_application_fundamentals(monkeypatch, installed_pkgs, events, releases, expected_target_pkgs):
    """Trivial checks validating that the core event application algorithm reflects event semantics as expected."""
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    actual_target_pkgs, dummy_demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, releases)

    # Perform strict comparison
    actual_pkg_tuple_set = {(pkg.name, pkg.repository, pkg.modulestream) for pkg in actual_target_pkgs}
    expected_pkg_tuple_set = {(pkg.name, pkg.repository, pkg.modulestream) for pkg in expected_target_pkgs}
    assert actual_pkg_tuple_set == expected_pkg_tuple_set


def test_compute_pkg_state(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    events = [
        Event(1, Action.SPLIT,
              {Package('original', 'rhel7-repo', None)},
              {Package('split01', 'rhel8-repo', None), Package('split02', 'rhel8-repo', None)},
              (7, 6), (8, 0), []),
        Event(2, Action.REMOVED,
              {Package('removed', 'rhel7-repo', None)}, set(),
              (7, 6), (8, 0), []),
        Event(3, Action.PRESENT,
              {Package('present', 'rhel8-repo', None)}, set(),
              (7, 6), (8, 0), []),
        Event(4, Action.REMOVED,
              {Package('reintroduced', 'rhel7-repo', None)}, set(),
              (7, 6), (8, 0), []),
        Event(5, Action.PRESENT,
              {Package('reintroduced', 'rhel8-repo', None)}, set(),
              (8, 0), (8, 1), []),
        Event(6, Action.PRESENT,
              set(), {Package('neverthere', 'rhel8-repo', None)},
              (8, 0), (8, 1), [])
    ]

    installed_pkgs = {
        Package('original', 'rhel7-repo', None),
        Package('removed', 'rhel7-repo', None),
        Package('present', 'rhel7-repo', None),
        Package('reintroduced', 'rhel7-repo', None),
    }

    target_pkgs, dummy_demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, [(8, 0), (8, 1)])

    expected_target_pkgs = {
        Package('split01', 'rhel8-repo', None),
        Package('split02', 'rhel8-repo', None),
        Package('present', 'rhel8-repo', None),
        Package('reintroduced', 'rhel8-repo', None),
    }
    assert target_pkgs == expected_target_pkgs


def test_compute_rpm_tasks_from_pkg_set_diff(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[EnabledModules(modules=[])]))
    source_pkgs = {
        Package('removed1', '7repo', None),
        Package('removed2', '7repo', None),
        Package('kept1', '7repo', None),
        Package('kept2', '7repo', None),
    }

    target_pkgs = {
        Package('kept1', '8repo0', None),
        Package('kept2', '8repo0', None),
        Package('installed1', '8repo1', None),
        Package('installed2', '8repo2', None),
    }

    rpm_tasks = compute_rpm_tasks_from_pkg_set_diff(source_pkgs, target_pkgs, set())

    assert rpm_tasks.to_install == ['installed1', 'installed2']
    assert rpm_tasks.to_remove == ['removed1', 'removed2']


def test_actor_performs(monkeypatch):
    """Test whether the actor performs as expected when supplied with required messages."""

    Pkg = partial(Package, modulestream=None)

    events = [
        Event(1, Action.SPLIT,
              {Pkg('split-in', 'rhel7-base')},
              {Pkg('split-out0', 'rhel8-BaseOS'), Pkg('split-out1', 'rhel8-BaseOS')},
              (7, 9), (8, 0), []),
        Event(2, Action.MERGED,
              {Pkg('split-out0', 'rhel8-BaseOS'), Pkg('split-out1', 'rhel8-BaseOS')},
              {Pkg('merged-out', 'rhel8-BaseOS')},
              (8, 0), (8, 1), []),
        Event(3, Action.MOVED,
              {Pkg('moved-in', 'rhel7-base')}, {Pkg('moved-out', 'rhel8-BaseOS')},
              (7, 9), (8, 0), []),
        Event(4, Action.REMOVED,
              {Pkg('removed', 'rhel7-base')}, set(),
              (8, 0), (8, 1), []),
        Event(5, Action.DEPRECATED,
              {Pkg('irrelevant', 'rhel7-base')}, set(),
              (8, 0), (8, 1), []),
    ]

    monkeypatch.setattr(pes_events_scanner, 'get_pes_events', lambda data_folder, json_filename: events)

    _RPM = partial(RPM, epoch='', packager='', version='', release='', arch='', pgpsig='')

    installed_pkgs = InstalledRedHatSignedRPM(items=[
        _RPM(name='split-in'), _RPM(name='moved-in'), _RPM(name='removed')
    ])

    repositories_mapping = RepositoriesMapping(
        mapping=[
            RepoMapEntry(source='rhel7-base', target=['rhel8-BaseOS'], ),
        ],
        repositories=[
            PESIDRepositoryEntry(pesid='rhel7-base', major_version='7', repoid='rhel7-repo', arch='x86_64',
                                 repo_type='rpm', channel='ga', rhui=''),
            PESIDRepositoryEntry(pesid='rhel8-BaseOS', major_version='8', repoid='rhel8-repo', arch='x86_64',
                                 repo_type='rpm', channel='ga', rhui='')]
    )

    enabled_modules = EnabledModules(modules=[])
    repo_facts = RepositoriesFacts(
        repositories=[RepositoryFile(file='', data=[RepositoryData(repoid='rhel7-repo', name='RHEL7 repo')])]
    )

    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(msgs=[installed_pkgs, repositories_mapping, enabled_modules, repo_facts],
                                           src_ver='7.9', dst_ver='8.1'))

    produced_messages = produce_mocked()
    created_report = create_report_mocked()
    monkeypatch.setattr(api, 'produce', produced_messages)
    monkeypatch.setattr(reporting, 'create_report', created_report)

    pes_events_scanner.process()

    assert produced_messages.called

    produced_rpm_tasks = [msg for msg in produced_messages.model_instances if isinstance(msg, PESRpmTransactionTasks)]
    expected_rpm_tasks = PESRpmTransactionTasks(to_install=['merged-out', 'moved-out'],
                                                to_remove=['moved-in', 'removed', 'split-in'],
                                                modules_to_enable=[],
                                                modules_to_reset=[])
    assert len(produced_rpm_tasks) == 1
    assert produced_rpm_tasks[0].to_install == expected_rpm_tasks.to_install
    assert produced_rpm_tasks[0].to_remove == expected_rpm_tasks.to_remove
    assert produced_rpm_tasks[0].modules_to_enable == expected_rpm_tasks.modules_to_enable
    assert produced_rpm_tasks[0].modules_to_reset == expected_rpm_tasks.modules_to_reset


def test_transaction_configuration_has_effect(monkeypatch):
    _Pkg = partial(Package, repository=None, modulestream=None)

    def mocked_transaction_conf():
        return TransactionConfiguration(
            to_install=[_Pkg('pkg-a'), _Pkg('pkg-b')],
            to_remove=[_Pkg('pkg-c'), _Pkg('pkg-d')],
            to_keep=[]
        )

    monkeypatch.setattr(pes_events_scanner, 'get_transaction_configuration', mocked_transaction_conf)

    packages = {_Pkg('pkg-a'), _Pkg('pkg-c')}
    _result = pes_events_scanner.apply_transaction_configuration(packages)
    result = {(p.name, p.repository, p.modulestream) for p in _result}
    expected = {('pkg-a', None, None), ('pkg-b', None, None)}

    assert result == expected


def test_repository_blacklist_is_correctly_applied(monkeypatch):
    _Pkg = partial(Package, modulestream=None)

    monkeypatch.setattr(pes_events_scanner, 'get_blacklisted_repoids', lambda: {'repo-a', 'repo-b', 'repo-c'})
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    source_pkgs = {_Pkg('pkg-b', 'repo-b')}
    target_pkgs = {_Pkg('pkg-a', 'repo-a'), _Pkg('pkg-b', 'repo-b'), _Pkg('pkg-c', 'repo-c'), _Pkg('pkg-d', 'repo-d')}

    blacklisted_repoids, target_pkgs = pes_events_scanner.remove_new_packages_from_blacklisted_repos(source_pkgs,
                                                                                                     target_pkgs)
    result = pkgs_into_tuples(target_pkgs)

    assert blacklisted_repoids == {'repo-a', 'repo-b', 'repo-c'}
    assert result == {('pkg-b', 'repo-b', None), ('pkg-d', 'repo-d', None)}

    assert reporting.create_report.called
    for removed_pkg_name in ('pkg-a', 'pkg-c'):
        assert removed_pkg_name in reporting.create_report.reports[0]['summary']


def test_blacklisted_repoid_is_not_produced(monkeypatch):
    """
    Test that upgrade with a package that would be from a blacklisted repository on the target system does not remove
    the package as it was already installed, however, the blacklisted repoid should not be produced.
    """
    installed_pkgs = {Package('pkg-a', 'blacklisted-rhel7', None), Package('pkg-b', 'repoid-rhel7', None)}
    events = [
        Event(1, Action.MOVED, {Package('pkg-b', 'repoid-rhel7', None)}, {Package('pkg-b', 'repoid-rhel8', None)},
              (8, 0), (8, 1), []),
        Event(2, Action.MOVED, {Package('pkg-a', 'repoid-rhel7', None)}, {Package('pkg-a', 'blacklisted-rhel8', None)},
              (8, 0), (8, 1), []),
    ]

    monkeypatch.setattr(pes_events_scanner, 'get_installed_pkgs', lambda: installed_pkgs)
    monkeypatch.setattr(pes_events_scanner, 'get_pes_events', lambda folder, filename: events)
    monkeypatch.setattr(pes_events_scanner, 'apply_transaction_configuration', lambda pkgs: pkgs)
    monkeypatch.setattr(pes_events_scanner, 'get_blacklisted_repoids', lambda: {'blacklisted-rhel8'})
    monkeypatch.setattr(pes_events_scanner, 'replace_pesids_with_repoids_in_packages',
                        lambda pkgs, src_pkgs_repoids: pkgs)

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())

    pes_events_scanner.process()

    assert not reporting.create_report.called

    rpm_tasks = [msg for msg in api.produce.model_instances if isinstance(msg, PESRpmTransactionTasks)]

    fail_desc = ('pkg-a should not be removed as it is already installed, and it just won\'t be upgraded since its '
                 'repository is blacklisted. All remaining pkgs are kept, no new pkgs are installed, and therefore, '
                 'no PESRpmTransactionTasks should be produced, however, they were.')
    assert not rpm_tasks, fail_desc

    repo_setup_tasks = [msg for msg in api.produce.model_instances if isinstance(msg, RepositoriesSetupTasks)]
    assert len(repo_setup_tasks) == 1
    assert repo_setup_tasks[0].to_enable == ['repoid-rhel8']


@pytest.mark.parametrize(
    ('installed_pkgs', 'expected_target_pkgs'),
    (
        ({Package('in', 'rhel7-repo', ('m', 's'))}, {('out', 'rhel8-repo-modular', ('m', 's'))}),
        ({Package('in', 'rhel7-repo', None)}, {('out', 'rhel8-repo', None)}),
    )
)
def test_modularity_info_distinguishes_pkgs(monkeypatch, installed_pkgs, expected_target_pkgs):
    events = [
        Event(1, Action.MOVED,
              {Package('in', 'rhel7-repo', None)}, {Package('out', 'rhel8-repo', None)},
              (8, 0), (8, 1), []),
        Event(2, Action.MOVED,
              {Package('in', 'rhel7-repo', ('m', 's'))}, {Package('out', 'rhel8-repo-modular', ('m', 's'))},
              (8, 0), (8, 1), []),
    ]

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    target_pkgs, dummy_demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, [(8, 1)])

    assert pkgs_into_tuples(target_pkgs) == expected_target_pkgs


def test_pkgs_are_demodularized_when_crossing_major_version(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9'))

    events = [
        Event(1, Action.MOVED,
              {Package('modular', 'repo1-in', ('module1', 'stream'))},
              {Package('modular', 'repo1-out', ('module2', 'stream'))},
              (7, 9), (8, 0), []),
    ]

    installed_pkgs = {
        Package('modular', 'repo1-in', ('module1', 'stream')),
        Package('demodularized', 'repo', ('module-demodularized', 'stream'))
    }

    target_pkgs, demodularized_pkgs = compute_packages_on_target_system(installed_pkgs, events, [(8, 0)])

    expected_target_pkgs = {
        Package('modular', 'repo1-out', ('module2', 'stream')),
        Package('demodularized', 'repo', None)
    }
    assert demodularized_pkgs == {Package('demodularized', 'repo', ('module-demodularized', 'stream'))}
    assert target_pkgs == expected_target_pkgs


# repos/system_upgrade/common/actors/preparepythonworkround/libraries/workaround.py

import os
import shutil
import sys

from leapp.libraries.common.utils import makedirs
from leapp.libraries.stdlib import api

LEAPP_HOME = '/root/tmp_leapp_py3'


def _get_python_dirname():
    # NOTE: I thought about the static value: python2.7 for el7, python3.6 for
    # el8; but in the end I've ratcher switched to this generic solution.
    return 'python{}.{}'.format(sys.version_info.major, sys.version_info.minor)


def _get_orig_leapp_path():
    return os.path.join('/usr/lib', _get_python_dirname(), 'site-packages/leapp')


def apply_python3_workaround():
    py3_leapp = os.path.join(LEAPP_HOME, 'leapp3')
    if os.path.exists(LEAPP_HOME):
        try:
            shutil.rmtree(LEAPP_HOME)
        except OSError as e:
            api.current_logger().error('Could not remove {} directory: {}'.format(LEAPP_HOME, str(e)))

    makedirs(LEAPP_HOME)
    leapp_lib_symlink_path = os.path.join(LEAPP_HOME, 'leapp')
    os.symlink(_get_orig_leapp_path(), leapp_lib_symlink_path)
    with open(py3_leapp, 'w') as f:
        f_content = [
            '#!/usr/bin/python3',
            'import sys',
            'sys.path.append(\'{}\')'.format(LEAPP_HOME),
            '',
            'import leapp.cli',
            'sys.exit(leapp.cli.main())',
        ]
        f.write('{}\n\n'.format('\n'.join(f_content)))
    os.chmod(py3_leapp, 0o770)


# repos/system_upgrade/common/actors/preparepythonworkround/tests/test_preparepythonworkaround.py

from collections import namedtuple
from os import access, path, symlink, X_OK

import pytest

from leapp.libraries.actor import workaround
from leapp.libraries.common.utils import makedirs

SysVersionInfo = namedtuple('version_info', ['major', 'minor', 'micro', 'releaselevel', 'serial'])


def fake_symlink(basedir):
    def impl(source, target):
        source_path = str(basedir.join(*source.lstrip('/').split('/')))
        makedirs(source_path)
        symlink(source_path, target)
    return impl


def test_apply_python3_workaround(monkeypatch, tmpdir):
    leapp_home = tmpdir.mkdir('tmp_leapp_py3')
    monkeypatch.setattr(workaround.os, 'symlink', fake_symlink(tmpdir.mkdir('lib')))
    monkeypatch.setattr(workaround, 'LEAPP_HOME', str(leapp_home))

    # Ensure double invocation doesn't cause a problem
    workaround.apply_python3_workaround()
    workaround.apply_python3_workaround()

    # Ensure creation of all required elements
    assert path.islink(str(leapp_home.join('leapp')))
    assert path.isfile(str(leapp_home.join('leapp3')))
    assert access(str(leapp_home.join('leapp3')), X_OK)

    assert str(leapp_home) in leapp_home.join('leapp3').read_text('utf-8')


@pytest.mark.parametrize('pydir', ('python2.7', 'python3.6', 'python3.9'))
def test_orig_leapp_path(monkeypatch, pydir):
    monkeypatch.setattr(workaround, '_get_python_dirname', lambda: pydir)
    assert workaround._get_orig_leapp_path() == '/usr/lib/{}/site-packages/leapp'.format(pydir)


@pytest.mark.parametrize('sys_version_info,result', (
    (SysVersionInfo(2, 7, 5, 'final', 0), 'python2.7'),
    (SysVersionInfo(3, 6, 0, 'X', 0), 'python3.6'),
    (SysVersionInfo(3, 9, 0, 'X', 0), 'python3.9'),
))
def test_get_python_dirname(monkeypatch, sys_version_info, result):
    monkeypatch.setattr(workaround.sys, 'version_info', sys_version_info)
    assert workaround._get_python_dirname() == result


# repos/system_upgrade/common/actors/redhatsignedrpmcheck/libraries/redhatsignedrpmcheck.py

from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.libraries.stdlib.config import is_verbose
from leapp.models import InstalledUnsignedRPM

COMMON_REPORT_TAGS = [reporting.Groups.SANITY]


def generate_report(packages):
    """ Generate a report if there are unsigned packages installed on the system """
    if not packages:
        return
    unsigned_packages_new_line = '\n'.join(['- ' + p for p in packages])
    title = 'Packages not signed by Red Hat found on the system'
    summary = ('The following packages have not been signed by Red Hat'
               ' and may be removed during the upgrade process in case Red Hat-signed'
               ' packages to be removed during the upgrade depend on them:\n{}'
               .format(unsigned_packages_new_line))
    reporting.create_report([
        reporting.Title(title),
        reporting.Summary(summary),
        reporting.Severity(reporting.Severity.HIGH),
        reporting.Groups(COMMON_REPORT_TAGS)
    ])

    if is_verbose():
        api.show_message(summary)


def get_unsigned_packages():
    """ Get list of unsigned packages installed in the system """
    rpm_messages = api.consume(InstalledUnsignedRPM)
    data = next(rpm_messages, InstalledUnsignedRPM())
    if list(rpm_messages):
        api.current_logger().warning('Unexpectedly received more than one InstalledUnsignedRPM message.')
    unsigned_packages = set()
    unsigned_packages.update([pkg.name for pkg in data.items])
    unsigned_packages = list(unsigned_packages)
    unsigned_packages.sort()
    return unsigned_packages


def check_unsigned_packages():
    """ Check and generate reports if system contains unsigned installed packages"""
    generate_report(get_unsigned_packages())


# repos/system_upgrade/common/actors/redhatsignedrpmcheck/tests/test_redhatsignedrpmcheck.py

from leapp import reporting
from leapp.libraries.actor import redhatsignedrpmcheck
from leapp.libraries.common.testutils import create_report_mocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import InstalledUnsignedRPM, RPM

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


def test_actor_execution_without_unsigned_data(monkeypatch):
    def consume_unsigned_message_mocked(*models):
        installed_rpm = []
        yield InstalledUnsignedRPM(items=installed_rpm)
    monkeypatch.setattr(api, "consume", consume_unsigned_message_mocked)
    monkeypatch.setattr(api, "produce", produce_mocked())
    monkeypatch.setattr(api, "show_message", lambda x: True)
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    packages = redhatsignedrpmcheck.get_unsigned_packages()
    assert not packages
    redhatsignedrpmcheck.generate_report(packages)
    assert reporting.create_report.called == 0


def test_actor_execution_with_unsigned_data(monkeypatch):
    def consume_unsigned_message_mocked(*models):
        installed_rpm = [
            RPM(name='sample02', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
                pgpsig='SOME_OTHER_SIG_X'),
            RPM(name='sample04', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
                pgpsig='SOME_OTHER_SIG_X'),
            RPM(name='sample06', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
                pgpsig='SOME_OTHER_SIG_X'),
            RPM(name='sample08', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
                pgpsig='SOME_OTHER_SIG_X')]
        yield InstalledUnsignedRPM(items=installed_rpm)

    monkeypatch.setattr(api, "consume", consume_unsigned_message_mocked)
    monkeypatch.setattr(api, "produce", produce_mocked())
    monkeypatch.setattr(api, "show_message", lambda x: True)
    monkeypatch.setattr(reporting, "create_report", create_report_mocked())

    packages = redhatsignedrpmcheck.get_unsigned_packages()
    assert len(packages) == 4
    redhatsignedrpmcheck.generate_report(packages)
    assert reporting.create_report.called == 1
    assert 'Packages not signed by Red Hat found' in reporting.create_report.report_fields['title']


# repos/system_upgrade/common/actors/redhatsignedrpmscanner/tests/test_redhatsignedrpmscanner.py

import mock

from leapp.libraries.common import rpms
from leapp.libraries.common.config import mock_configs
from leapp.models import (
    fields,
    InstalledRedHatSignedRPM,
    InstalledRPM,
    InstalledUnsignedRPM,
    IPUConfig,
    Model,
    RPM
)

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


class MockObject(Model):
    topic = RPM.topic
    value = fields.Integer(default=42)
    plan = fields.Nullable(fields.String())


class MockModel(Model):
    topic = RPM.topic
    list_field = fields.List(fields.Model(MockObject), default=[])
    list_field_nullable = fields.Nullable(fields.List(fields.String()))
    int_field = fields.Integer(default=42)


def test_no_installed_rpms(current_actor_context):
    current_actor_context.run(config_model=mock_configs.CONFIG)
    assert current_actor_context.consume(InstalledRedHatSignedRPM)
    assert current_actor_context.consume(InstalledUnsignedRPM)


def test_actor_execution_with_signed_unsigned_data(current_actor_context):
    installed_rpm = [
        RPM(name='sample01', 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='sample02', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_OTHER_SIG_X'),
        RPM(name='sample03', 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 5326810137017186'),
        RPM(name='sample04', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_OTHER_SIG_X'),
        RPM(name='sample05', 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 938a80caf21541eb'),
        RPM(name='sample06', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_OTHER_SIG_X'),
        RPM(name='sample07', 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 fd372689897da07a'),
        RPM(name='sample08', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_OTHER_SIG_X'),
        RPM(name='sample09', 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 45689c882fa658e0')]

    current_actor_context.feed(InstalledRPM(items=installed_rpm))
    current_actor_context.run(config_model=mock_configs.CONFIG)
    assert current_actor_context.consume(InstalledRedHatSignedRPM)
    assert len(current_actor_context.consume(InstalledRedHatSignedRPM)[0].items) == 5
    assert current_actor_context.consume(InstalledUnsignedRPM)
    assert len(current_actor_context.consume(InstalledUnsignedRPM)[0].items) == 4


def test_all_rpms_signed(current_actor_context):
    installed_rpm = [
        RPM(name='sample01', 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='sample02', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_OTHER_SIG_X'),
        RPM(name='sample03', 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 5326810137017186'),
        RPM(name='sample04', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_OTHER_SIG_X')
    ]

    current_actor_context.feed(InstalledRPM(items=installed_rpm))
    current_actor_context.run(config_model=mock_configs.CONFIG_ALL_SIGNED)
    assert current_actor_context.consume(InstalledRedHatSignedRPM)
    assert len(current_actor_context.consume(InstalledRedHatSignedRPM)[0].items) == 4
    assert not current_actor_context.consume(InstalledUnsignedRPM)[0].items


def test_katello_pkg_goes_to_signed(current_actor_context):
    installed_rpm = [
        RPM(name='katello-ca-consumer-vm-098.example.com',
            version='1.0',
            release='1',
            epoch='0',
            packager='None',
            arch='noarch',
            pgpsig=''),
    ]

    current_actor_context.feed(InstalledRPM(items=installed_rpm))
    current_actor_context.run(config_model=mock_configs.CONFIG_ALL_SIGNED)
    assert current_actor_context.consume(InstalledRedHatSignedRPM)
    assert len(current_actor_context.consume(InstalledRedHatSignedRPM)[0].items) == 1
    assert not current_actor_context.consume(InstalledUnsignedRPM)[0].items


def test_gpg_pubkey_pkg(current_actor_context):
    installed_rpm = [
        RPM(name='gpg-pubkey', 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 aa17105e03152d37'),
        RPM(name='gpg-pubkey', version='0.1', release='1.sm01', epoch='1', packager='Tester', arch='noarch',
            pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM -03, Key ID 9ea903b1361e896b'),
    ]

    current_actor_context.feed(InstalledRPM(items=installed_rpm))
    current_actor_context.run(config_model=mock_configs.CONFIG)
    assert current_actor_context.consume(InstalledRedHatSignedRPM)
    assert len(current_actor_context.consume(InstalledRedHatSignedRPM)[0].items) == 1
    assert current_actor_context.consume(InstalledUnsignedRPM)
    assert len(current_actor_context.consume(InstalledUnsignedRPM)[0].items) == 1


def test_create_lookup():
    # NOTE(ivasilev) Ideally should be tested separately from the actor, but since library
    # testing functionality is not yet implemented in leapp-repository the tests will reside here.
    model = MockModel(list_field=[MockObject(value=42, plan="A"),
                                  MockObject(value=-42, plan="B"),
                                  MockObject(value=9999)])
    # plain non-empty list
    keys = ('value', )
    with mock.patch('leapp.libraries.stdlib.api.consume', return_value=(model,)):
        lookup = rpms.create_lookup(MockModel, 'list_field', keys=keys)
        assert {(42, ), (-42, ), (9999, )} == lookup
    # plain list, multiple keys
    with mock.patch('leapp.libraries.stdlib.api.consume', return_value=(model,)):
        lookup = rpms.create_lookup(MockModel, 'list_field', keys=('value', 'plan'))
        assert {(42, 'A'), (-42, 'B'), (9999, None)} == lookup
    # empty list
    model.list_field = []
    with mock.patch('leapp.libraries.stdlib.api.consume', return_value=(model,)):
        lookup = rpms.create_lookup(MockModel, 'list_field', keys=keys)
        assert set() == lookup
    # nullable list without default
    assert model.list_field_nullable is None
    with mock.patch('leapp.libraries.stdlib.api.consume', return_value=(model,)):
        lookup = rpms.create_lookup(MockModel, 'list_field_nullable', keys=keys)
        assert set() == lookup
    # improper usage: lookup from non iterable field
    with mock.patch('leapp.libraries.stdlib.api.consume', return_value=(model,)):
        lookup = rpms.create_lookup(MockModel, 'int_field', keys=keys)
        assert set() == lookup
    # improper usage: lookup from iterable but bad attribute
    with mock.patch('leapp.libraries.stdlib.api.consume', return_value=(model,)):
        lookup = rpms.create_lookup(MockModel, 'list_field', keys=('nosuchattr',))
        assert set() == lookup
    # improper usage: lookup from iterable, multiple keys bad 1 bad
    with mock.patch('leapp.libraries.stdlib.api.consume', return_value=(model,)):
        lookup = rpms.create_lookup(MockModel, 'list_field', keys=('value', 'nosuchattr'))
        assert set() == lookup


def test_has_package(current_actor_context):
    installed_rpm = [
        RPM(name='sample01', 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='sample02', version='0.1', release='1.sm01', epoch='1', packager=RH_PACKAGER, arch='noarch',
            pgpsig='SOME_OTHER_SIG_X'),
    ]

    current_actor_context.feed(InstalledRPM(items=installed_rpm))
    current_actor_context.run(config_model=mock_configs.CONFIG)
    assert rpms.has_package(InstalledRedHatSignedRPM, 'sample01', context=current_actor_context)
    assert not rpms.has_package(InstalledRedHatSignedRPM, 'nosuchpackage', context=current_actor_context)
    assert rpms.has_package(InstalledUnsignedRPM, 'sample02', context=current_actor_context)
    assert not rpms.has_package(InstalledUnsignedRPM, 'nosuchpackage', context=current_actor_context)


# repos/system_upgrade/common/actors/removebootfiles/libraries/removebootfiles.py

import os

from leapp.exceptions import StopActorExecution
from leapp.libraries.stdlib import api
from leapp.models import BootContent


def remove_boot_files():
    boot_content_msgs = api.consume(BootContent)
    boot_content = next(boot_content_msgs, None)
    if list(boot_content_msgs):
        api.current_logger().warning('Unexpectedly received more than one BootContent message.')
    if not boot_content:
        api.current_logger().warning('Did not receive a message about the leapp-provided kernel and initramfs ->'
                                     ' Skipping removal of these files.')
        raise StopActorExecution
    for filepath in boot_content.kernel_path, boot_content.initram_path:
        remove_file(filepath)


def remove_file(filepath):
    try:
        os.remove(filepath)
    except OSError as err:
        api.current_logger().error('Could not remove {0}: {1}.'.format(filepath, err))


# repos/system_upgrade/common/actors/removebootfiles/tests/unit_test_removebootfiles.py

import pytest

from leapp.exceptions import StopActorExecution
from leapp.libraries.actor import removebootfiles
from leapp.libraries.common.testutils import logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import BootContent


class remove_file_mocked(object):
    def __init__(self):
        self.called = 0
        self.files_to_remove = []

    def __call__(self, filename):
        self.called += 1
        self.files_to_remove.append(filename)


def test_remove_boot_files(monkeypatch):
    # BootContent message available
    def consume_message_mocked(*models):
        yield BootContent(kernel_path='/abc', initram_path='/def')

    monkeypatch.setattr('leapp.libraries.stdlib.api.consume', consume_message_mocked)
    monkeypatch.setattr(removebootfiles, 'remove_file', remove_file_mocked())

    removebootfiles.remove_boot_files()

    assert removebootfiles.remove_file.files_to_remove == ['/abc', '/def']

    # No BootContent message available
    def consume_no_message_mocked(*models):
        yield None

    monkeypatch.setattr('leapp.libraries.stdlib.api.consume', consume_no_message_mocked)
    monkeypatch.setattr(removebootfiles, 'remove_file', remove_file_mocked())
    monkeypatch.setattr('leapp.libraries.stdlib.api.current_logger', logger_mocked())

    with pytest.raises(StopActorExecution):
        removebootfiles.remove_boot_files()

    assert removebootfiles.remove_file.called == 0
    assert any("Did not receive a message" in msg for msg in api.current_logger.warnmsg)


def test_remove_file_that_does_not_exist(monkeypatch):
    def remove_mocked(filepath):
        raise OSError

    monkeypatch.setattr('os.remove', remove_mocked)
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    removebootfiles.remove_file('/filepath')

    assert any("Could not remove /filepath" in msg for msg in api.current_logger.errmsg)


# repos/system_upgrade/common/actors/removeresumeservice/tests/test_removeresumeservice.py

import errno
import os

import pytest


@pytest.mark.skipif(
    not os.path.isdir('/etc/systemd/system/default.target.wants')
    or not os.getuid() == 0,
    reason='default.target.wants dir should exists and test should be run '
           'under the root user.',
)
# TODO make the test not destructive
@pytest.mark.skipif(os.getenv("DESTRUCTIVE_TESTING", False) in [False, "0"],
                    reason='Test disabled by default because it would modify the system')
def test_remove_resume_service(current_actor_context):
    service_name = 'leapp_resume.service'
    service_path = os.path.join('/etc/systemd/system/', service_name)
    symlink_path = os.path.join('/etc/systemd/system/default.target.wants/', service_name)

    # lets make sure there are not leftovers from previous tests
    try:
        os.unlink(service_path)
        os.unlink(symlink_path)
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise

    with open(service_path, 'w'):
        pass

    os.symlink(service_path, symlink_path)

    current_actor_context.run()

    assert not os.path.isfile(service_path)
    assert not os.path.isfile(symlink_path)


# repos/system_upgrade/common/actors/removeupgradebootentry/libraries/removeupgradebootentry.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import BootContent, FirmwareFacts


def remove_boot_entry():
    # we need to make sure /boot is mounted before trying to remove the boot entry

    facts_msg = api.consume(FirmwareFacts)
    facts = next(facts_msg, None)
    if not facts:
        raise StopActorExecutionError('Could not identify system firmware',
                                      details={'details': 'Actor did not receive FirmwareFacts message.'})

    mount_points_per_firmware = {
        'bios': ['/boot'],
        'efi': ['/boot', '/boot/efi']
    }

    for mp in mount_points_per_firmware[facts.firmware]:
        try:
            run(['/bin/mount', mp])
        except CalledProcessError:
            # partitions have been most likely already mounted
            pass
    kernel_filepath = get_upgrade_kernel_filepath()
    run([
        '/usr/sbin/grubby',
        '--remove-kernel={0}'.format(kernel_filepath)
    ])
    if architecture.matches_architecture(architecture.ARCH_S390X):
        # on s390x we need to call zipl explicitly because of issue in grubby,
        # otherwise the new boot entry will not be set as default
        # See https://bugzilla.redhat.com/show_bug.cgi?id=1764306
        run(['/usr/sbin/zipl'])

    # TODO: Move calling `mount -a` to a separate actor as it is not really related to removing the upgrade boot entry.
    #       It's worth to call it after removing the boot entry to avoid boot loop in case mounting fails.
    run([
        '/bin/mount', '-a'
    ])


def get_upgrade_kernel_filepath():
    boot_content_msgs = api.consume(BootContent)
    boot_content = next(boot_content_msgs, None)
    if list(boot_content_msgs):
        api.current_logger().warning('Unexpectedly received more than one BootContent message.')
    if not boot_content:
        raise StopActorExecutionError('Could not remove GRUB boot entry for the upgrade initramfs.',
                                      details={'details': 'Did not receive a message about the leapp-provided'
                                                          ' kernel and initramfs.'})
    return boot_content.kernel_path


# repos/system_upgrade/common/actors/removeupgradebootentry/tests/unit_test_removeupgradebootentry.py

import pytest

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import removeupgradebootentry
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import BootContent, FirmwareFacts


class run_mocked(object):
    args = []

    def __call__(self, args, split=True):
        self.args.append(args)


@pytest.mark.parametrize('firmware', ['bios', 'efi'])
@pytest.mark.parametrize('arch', [architecture.ARCH_X86_64, architecture.ARCH_S390X])
def test_remove_boot_entry(firmware, arch, monkeypatch):
    def get_upgrade_kernel_filepath_mocked():
        return '/abc'

    def consume_systemfacts_mocked(*models):
        yield FirmwareFacts(firmware=firmware)

    monkeypatch.setattr(removeupgradebootentry, 'get_upgrade_kernel_filepath', get_upgrade_kernel_filepath_mocked, )
    monkeypatch.setattr(api, 'consume', consume_systemfacts_mocked)
    monkeypatch.setattr(removeupgradebootentry, 'run', run_mocked())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch))
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    removeupgradebootentry.remove_boot_entry()

    boot_mounts = [['/bin/mount', '/boot']]
    if firmware == 'efi':
        boot_mounts.append(['/bin/mount', '/boot/efi'])

    calls = boot_mounts + [['/usr/sbin/grubby', '--remove-kernel=/abc']]
    if arch == architecture.ARCH_S390X:
        calls.append(['/usr/sbin/zipl'])
    calls.append(['/bin/mount', '-a'])

    assert removeupgradebootentry.run.args == calls

    # clear args for next run
    del removeupgradebootentry.run.args[:]


def test_get_upgrade_kernel_filepath(monkeypatch):
    # BootContent message available
    def consume_message_mocked(*models):
        yield BootContent(kernel_path='/abc', initram_path='/def')

    monkeypatch.setattr(api, 'consume', consume_message_mocked)

    kernel_path = removeupgradebootentry.get_upgrade_kernel_filepath()

    assert kernel_path == '/abc'

    # No BootContent message available
    def consume_no_message_mocked(*models):
        yield None

    monkeypatch.setattr(api, 'consume', consume_no_message_mocked)

    with pytest.raises(StopActorExecutionError):
        removeupgradebootentry.get_upgrade_kernel_filepath()


# repos/system_upgrade/common/actors/reportsettargetrelease/libraries/reportsettargetrelease.py

from leapp import reporting
from leapp.libraries.common import rhsm
from leapp.libraries.stdlib import api


def _report_set_release():
    target_version = api.current_actor().configuration.version.target
    reporting.create_report([
        reporting.Title(
            'The subscription-manager release is going to be set after the upgrade'),
        reporting.Summary(
            'After the upgrade has completed the release of the subscription-manager will be set to {release}.'
            ' This will ensure that you will receive and keep the version you choose to upgrade to.'
            .format(release=target_version)
        ),
        reporting.Severity(reporting.Severity.LOW),
        reporting.Remediation(
            hint='If you wish to receive updates for the latest released version of the target system, '
                 'run `subscription-manager release --unset` after the upgrade.'
        ),
        reporting.Groups([reporting.Groups.UPGRADE_PROCESS]),
        reporting.RelatedResource('package', 'subscription-manager')
    ])


def _report_unhandled_release():
    # TODO: set the POST group after it's created.
    target_version = api.current_actor().configuration.version.target
    hint_command = 'subscription-manager release --set {}'.format(target_version)
    # FIXME: This should use Dialogs and Answers to offer post-upgrade remediation
    # so that users can choose whether to --set or --unset the release number
    hint = 'Set the new release (or unset it) after the upgrade using subscription-manager: ' + hint_command
    reporting.create_report([
        reporting.Title(
            'The subscription-manager release is going to be kept as it is during the upgrade'),
        reporting.Summary(
            'The upgrade is executed with the --no-rhsm option (or with'
            ' the LEAPP_NO_RHSM environment variable). In this case, the subscription-manager'
            ' will not be configured during the upgrade. If the system is subscribed and release'
            ' is set already, you could encounter issues to get RHEL content using DNF/YUM'
            ' after the upgrade.'
        ),
        reporting.Severity(reporting.Severity.LOW),
        reporting.Remediation(hint=hint),
        reporting.Groups([reporting.Groups.UPGRADE_PROCESS]),
        reporting.RelatedResource('package', 'subscription-manager')
    ])


def process():
    if rhsm.skip_rhsm():
        _report_unhandled_release()
    else:
        _report_set_release()


# repos/system_upgrade/common/actors/reportsettargetrelease/tests/test_targetreleasereport_reportsettargetrelease.py

import pytest

from leapp import reporting
from leapp.libraries.actor import reportsettargetrelease
from leapp.libraries.common import rhsm
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked
from leapp.libraries.stdlib import api


@pytest.mark.parametrize('version', ['8.{}'.format(i) for i in range(4)])
def test_report_target_version(monkeypatch, version):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver=version))
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    SUMMARY_FMT = 'will be set to {}.'
    reportsettargetrelease.process()
    assert reporting.create_report.called == 1
    assert SUMMARY_FMT.format(version) in reporting.create_report.report_fields['summary']
    assert 'is going to be set' in reporting.create_report.report_fields['title']


def test_report_unhandled_release(monkeypatch):
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver='8.1'))
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: True)
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    reportsettargetrelease.process()
    assert reporting.create_report.called == 1
    assert 'is going to be kept' in reporting.create_report.report_fields['title']


# repos/system_upgrade/common/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.Groups([reporting.Groups.REPOSITORY]),
    ]
    reporting.create_report(report)


def _report_excluded_repos(repos):
    optional_repository_name = 'optional' if get_source_major_version() == '7' else 'CRB'
    api.current_logger().info(
        "The {0} repository is not enabled. Excluding {1} from the upgrade".format(optional_repository_name, 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.Groups([reporting.Groups.REPOSITORY]),
        reporting.Groups([reporting.Groups.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/common/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/common/actors/repositoriesmapping/libraries/repositoriesmapping.py

import json
import os
from collections import defaultdict

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
from leapp.libraries.common.fetch import read_or_fetch
from leapp.libraries.stdlib import api
from leapp.models import PESIDRepositoryEntry, RepoMapEntry, RepositoriesMapping
from leapp.models.fields import ModelViolationError

OLD_REPOMAP_FILE = 'repomap.csv'
"""The name of the old, deprecated repository mapping file (no longer used)."""

REPOMAP_FILE = 'repomap.json'
"""The name of the new repository mapping file."""


class RepoMapData(object):
    VERSION_FORMAT = '1.0.0'

    def __init__(self):
        self.repositories = []
        self.mapping = {}

    def add_repository(self, data, pesid):
        """
        Add new PESIDRepositoryEntry with given pesid from the provided dictionary.

        :param data: A dict containing the data of the added repository. The dictionary structure corresponds
                     to the repositories entries in the repository mapping JSON schema.
        :type data: Dict[str, str]
        :param pesid: PES id of the repository family that the newly added repository belongs to.
        :type pesid: str
        """
        self.repositories.append(PESIDRepositoryEntry(
            repoid=data['repoid'],
            channel=data['channel'],
            rhui=data.get('rhui', ''),
            repo_type=data['repo_type'],
            arch=data['arch'],
            major_version=data['major_version'],
            pesid=pesid
        ))

    def get_repositories(self, valid_major_versions):
        """
        Return the list of PESIDRepositoryEntry object matching the specified major versions.
        """
        return [repo for repo in self.repositories if repo.major_version in valid_major_versions]

    def add_mapping(self, source_major_version, target_major_version, source_pesid, target_pesid):
        """
        Add a new mapping entry that is mapping the source pesid to the destination pesid(s),
        relevant in an IPU from the supplied source major version to the supplied target
        major version.

        :param str source_major_version: Specifies the major version of the source system
                                         for which the added mapping applies.
        :param str target_major_version: Specifies the major version of the target system
                                         for which the added mapping applies.
        :param str source_pesid: PESID of the source repository.
        :param Union[str|List[str]] target_pesid: A single target PESID or a list of target
                                                  PESIDs of the added mapping.
        """
        # NOTE: it could be more simple, but I prefer to be sure the input data
        # contains just one map per source PESID.
        key = '{}:{}'.format(source_major_version, target_major_version)
        rmap = self.mapping.get(key, defaultdict(set))
        self.mapping[key] = rmap
        if isinstance(target_pesid, list):
            rmap[source_pesid].update(target_pesid)
        else:
            rmap[source_pesid].add(target_pesid)

    def get_mappings(self, src_major_version, dst_major_version):
        """
        Return the list of RepoMapEntry objects for the specified upgrade path.

        IOW, the whole mapping for specified IPU.
        """
        key = '{}:{}'.format(src_major_version, dst_major_version)
        rmap = self.mapping.get(key, None)
        if not rmap:
            return None
        map_list = []
        for src_pesid in sorted(rmap.keys()):
            map_list.append(RepoMapEntry(source=src_pesid, target=sorted(rmap[src_pesid])))
        return map_list

    @staticmethod
    def load_from_dict(data):
        if data['version_format'] != RepoMapData.VERSION_FORMAT:
            raise ValueError(
                'The obtained repomap data has unsupported version of format.'
                ' Get {} required {}'
                .format(data['version_format'], RepoMapData.VERSION_FORMAT)
            )

        repomap = RepoMapData()

        # Load reposiories
        existing_pesids = set()
        for repo_family in data['repositories']:
            existing_pesids.add(repo_family['pesid'])
            for repo in repo_family['entries']:
                repomap.add_repository(repo, repo_family['pesid'])

        # Load mappings
        for mapping in data['mapping']:
            for entry in mapping['entries']:
                if not isinstance(entry['target'], list):
                    raise ValueError(
                        'The target field of a mapping entry is not a list: {}'
                        .format(entry)
                    )

                for pesid in [entry['source']] + entry['target']:
                    if pesid not in existing_pesids:
                        raise ValueError(
                            'The {} pesid is not related to any repository.'
                            .format(pesid)
                        )
                repomap.add_mapping(
                    source_major_version=mapping['source_major_version'],
                    target_major_version=mapping['target_major_version'],
                    source_pesid=entry['source'],
                    target_pesid=entry['target'],
                )
        return repomap


def _inhibit_upgrade(msg):
    raise StopActorExecutionError(
        msg,
        details={'hint': ('Read documentation at the following link for more'
                          ' information about how to retrieve the valid file:'
                          ' https://access.redhat.com/articles/3664871')})


def _read_repofile(repofile):
    # NOTE: what about catch StopActorExecution error when the file cannot be
    # obtained -> then check whether old_repomap file exists and in such a case
    # inform user they have to provde the new repomap.json file (we have the
    # warning now only which could be potentially overlooked)
    try:
        return json.loads(read_or_fetch(repofile))
    except ValueError:
        # The data does not contain a valid json
        _inhibit_upgrade('The repository mapping file is invalid: file does not contain a valid JSON object.')
    return None  # Avoids inconsistent-return-statements warning


def scan_repositories(read_repofile_func=_read_repofile):
    """
    Scan the repository mapping file and produce RepositoriesMap msg.

    See the description of the actor for more details.
    """
    # TODO: add filter based on the current arch
    # TODO: deprecate the product type and introduce the "channels" ?.. more or less
    # NOTE: product type is changed, now it's channel: eus,e4s,aus,tus,ga,beta

    if os.path.exists(os.path.join('/etc/leapp/files', OLD_REPOMAP_FILE)):
        # NOTE: what about creating the report (instead of warning)
        api.current_logger().warning(
            'The old repomap file /etc/leapp/files/repomap.csv is present.'
            ' The file has been replaced by the repomap.json file and it is'
            ' not used anymore.'
        )

    json_data = read_repofile_func(REPOMAP_FILE)
    try:
        repomap_data = RepoMapData.load_from_dict(json_data)
        mapping = repomap_data.get_mappings(get_source_major_version(), get_target_major_version())

        valid_major_versions = [get_source_major_version(), get_target_major_version()]
        api.produce(RepositoriesMapping(
            mapping=mapping,
            repositories=repomap_data.get_repositories(valid_major_versions)
        ))
    except ModelViolationError as err:
        err_message = (
            'The repository mapping file is invalid: '
            'the JSON does not match required schema (wrong field type/value): {}'
            .format(err)
        )
        _inhibit_upgrade(err_message)
    except KeyError as err:
        _inhibit_upgrade(
            'The repository mapping file is invalid: the JSON is missing a required field: {}'.format(err))
    except ValueError as err:
        # The error should contain enough information, so we do not need to clarify it further
        _inhibit_upgrade('The repository mapping file is invalid: {}'.format(err))


# repos/system_upgrade/common/actors/repositoriesmapping/tests/unit_test_repositoriesmapping.py

import json
import os

import pytest
import requests

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import repositoriesmapping
from leapp.libraries.common import fetch
from leapp.libraries.common.config import architecture, version
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import PESIDRepositoryEntry

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)


def test_scan_existing_valid_data(monkeypatch, adjust_cwd):
    """
    Tests whether an existing valid repomap file is loaded correctly.
    """

    with open('files/repomap_example.json') as repomap_file:
        data = json.load(repomap_file)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    repositoriesmapping.scan_repositories(lambda dummy: data)

    assert api.produce.called, 'Actor did not produce any message when deserializing valid repomap data.'

    fail_description = 'Actor produced multiple messages, but only one was expected.'
    assert len(api.produce.model_instances) == 1, fail_description

    repo_mapping = api.produce.model_instances[0]

    # Verify that the loaded JSON data is matching the repomap file content
    # 1. Verify src_pesid -> target_pesids mappings are loaded and filtered correctly
    fail_description = 'Actor produced more mappings than there are source system relevant mappings in the test file.'
    assert len(repo_mapping.mapping) == 1, fail_description
    fail_description = 'Actor failed to load IPU-relevant mapping data correctly.'
    assert repo_mapping.mapping[0].source == 'pesid1', fail_description
    assert set(repo_mapping.mapping[0].target) == {'pesid2', 'pesid3'}, fail_description

    # 2. Verify that only repositories valid for the current IPU are produced
    pesid_repos = repo_mapping.repositories
    fail_description = 'Actor produced incorrect number of IPU-relevant pesid repos.'
    assert len(pesid_repos) == 3, fail_description

    expected_pesid_repos = [
        PESIDRepositoryEntry(
            pesid='pesid1',
            major_version='7',
            repoid='some-rhel-7-repoid',
            arch='x86_64',
            repo_type='rpm',
            channel='eus',
            rhui=''
        ),
        PESIDRepositoryEntry(
            pesid='pesid2',
            major_version='8',
            repoid='some-rhel-8-repoid1',
            arch='x86_64',
            repo_type='rpm',
            channel='eus',
            rhui=''
        ),
        PESIDRepositoryEntry(
            pesid='pesid3',
            major_version='8',
            repoid='some-rhel-8-repoid2',
            arch='x86_64',
            repo_type='rpm',
            channel='eus',
            rhui=''
        ),
    ]

    fail_description = 'Expected pesid repo is not present in the deserialization output.'
    for expected_pesid_repo in expected_pesid_repos:
        assert expected_pesid_repo in pesid_repos, fail_description


def test_scan_repositories_with_missing_data(monkeypatch):
    """
    Tests whether the scanning process fails gracefully when no data are read.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(repositoriesmapping, 'read_or_fetch', lambda dummy: '')

    with pytest.raises(StopActorExecutionError) as missing_data_error:
        repositoriesmapping.scan_repositories()
    assert 'does not contain a valid JSON object' in str(missing_data_error)


def test_scan_repositories_with_empty_data(monkeypatch):
    """
    Tests whether the scanning process fails gracefully when empty json data received.
    """

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    with pytest.raises(StopActorExecutionError) as empty_data_error:
        repositoriesmapping.scan_repositories(lambda dummy: {})
    assert 'the JSON is missing a required field' in str(empty_data_error)


@pytest.mark.parametrize('version_format', ('0.0.0', '1.0.1', '1.1.0', '2.0.0'))
def test_scan_repositories_with_bad_json_data_version(monkeypatch, version_format):
    """
    Tests whether the json data is checked for the version field and error is raised if the version
    does not match the latest one.
    """

    json_data = {
        'datetime': '202107141655Z',
        'version_format': version_format,
        'mapping': [],
        'repositories': []
    }

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    with pytest.raises(StopActorExecutionError) as bad_version_error:
        repositoriesmapping.scan_repositories(lambda dummy: json_data)

    assert 'mapping file is invalid' in str(bad_version_error)


def test_scan_repositories_with_mapping_to_pesid_without_repos(monkeypatch):
    """
    Tests that the loading of repositories mapping recognizes when there is a mapping with target pesid that does
    not have any repositories and inhibits the upgrade.
    """
    json_data = {
        'datetime': '202107141655Z',
        'version_format': repositoriesmapping.RepoMapData.VERSION_FORMAT,
        'mapping': [
            {
                'source_major_version': '7',
                'target_major_version': '8',
                'entries':  [
                    {
                        'source': 'source_pesid',
                        'target': ['nonexisting_pesid']
                    }
                ]
            }
        ],
        'repositories': [
            {
                'pesid': 'source_pesid',
                'entries': [
                    {
                        'major_version': '7',
                        'repoid': 'some-rhel-7-repo',
                        'arch': 'x86_64',
                        'repo_type': 'rpm',
                        'channel': 'eus'
                    }
                ]
            }
        ]
    }

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    with pytest.raises(StopActorExecutionError) as error_info:
        repositoriesmapping.scan_repositories(lambda dummy: json_data)

    assert 'pesid is not related to any repository' in error_info.value.message


def test_scan_repositories_with_repo_entry_missing_required_fields(monkeypatch):
    """
    Tests whether deserialization of pesid repo entries missing some of the required fields
    is handled internally and StopActorExecutionError is propagated to the user.
    """

    json_data = {
        'datetime': '202107141655Z',
        'version_format': repositoriesmapping.RepoMapData.VERSION_FORMAT,
        'mapping': [
            {
                'source_major_version': '7',
                'target_major_version': '8',
                'entries':  [
                    {
                        'source': 'source_pesid',
                        'target': ['target_pesid']
                    }
                ]
            }
        ],
        'repositories': [
            {
                'pesid': 'source_pesid',
                'entries': [
                    {
                        'major_version': '7',
                        'repoid': 'some-rhel-9-repo1',
                        'arch': 'x86_64',
                    }
                ]
            },
            {
                'pesid': 'target_pesid',
                'entries': [
                    {
                        'major_version': '7',
                        'repoid': 'some-rhel-9-repo1',
                        'arch': 'x86_64',
                    }
                ]
            }
        ]
    }

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    with pytest.raises(StopActorExecutionError) as error_info:
        repositoriesmapping.scan_repositories(lambda dummy: json_data)

    assert 'the JSON is missing a required field' in error_info.value.message


def test_scan_repositories_with_repo_entry_mapping_target_not_a_list(monkeypatch):
    """
    Tests whether deserialization of a mapping entry that has its target field set to a string
    is handled internally and StopActorExecutionError is propagated to the user.
    """

    json_data = {
        'datetime': '202107141655Z',
        'version_format': repositoriesmapping.RepoMapData.VERSION_FORMAT,
        'mapping': [
            {
                'source_major_version': '7',
                'target_major_version': '8',
                'entries':  [
                    {
                        'source': 'source_pesid',
                        'target': 'target_pesid'
                    }
                ]
            }
        ],
        'repositories': [
            {
                'pesid': 'source_pesid',
                'entries': [
                    {
                        'major_version': '7',
                        'repoid': 'some-rhel-9-repo1',
                        'arch': 'x86_64',
                    }
                ]
            },
            {
                'pesid': 'target_pesid',
                'entries': [
                    {
                        'major_version': '7',
                        'repoid': 'some-rhel-9-repo1',
                        'arch': 'x86_64',
                    }
                ]
            }
        ]
    }

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(src_ver='7.9', dst_ver='8.4'))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    with pytest.raises(StopActorExecutionError) as error_info:
        repositoriesmapping.scan_repositories(lambda dummy: json_data)

    assert 'repository mapping file is invalid' in error_info.value.message


# repos/system_upgrade/common/actors/rpmscanner/libraries/rpmscanner.py

import warnings

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import module as module_lib
from leapp.libraries.common import rpms
from leapp.libraries.stdlib import api
from leapp.models import InstalledRPM, RPM

no_yum = False
no_yum_warning_msg = "package `yum` is unavailable"
try:
    import yum
except ImportError:
    no_yum = True
    warnings.warn(no_yum_warning_msg, ImportWarning)

no_dnf = False
no_dnf_warning_msg = "package `dnf` is unavailable"
try:
    import dnf
except ImportError:
    no_dnf = True
    warnings.warn(no_dnf_warning_msg, ImportWarning)


def _get_package_repository_data_yum():
    yum_base = yum.YumBase()
    pkg_repos = {}

    try:
        for pkg in yum_base.doPackageLists().installed:
            pkg_repos[pkg.name] = pkg.ui_from_repo.lstrip('@')
    except ValueError as e:
        if 'locale' not in str(e):  # reraise if error is not related to locales
            raise e
        raise StopActorExecutionError(
            message='Failed to get installed RPM packages because of an invalid locale',
            details={
                'hint': 'Please run leapp with a valid locale. ' +
                        'You can get a list of installed locales by running `locale -a`.'
            })

    return pkg_repos


def _get_package_repository_data_dnf():
    dnf_base = dnf.Base()
    pkg_repos = {}

    try:
        # NOTE: currently we do not initialize/load DNF plugins here as we are
        # working just with the local stuff (load_system_repo=True)
        dnf_base.fill_sack(load_system_repo=True, load_available_repos=False)
        for pkg in dnf_base.sack.query():
            pkg_repos[pkg.name] = pkg._from_repo.lstrip('@')
    except ValueError as e:
        if 'locale' not in str(e):  # reraise if error is not related to locales
            raise e
        raise StopActorExecutionError(
            message='Failed to get installed RPM packages because of an invalid locale',
            details={
                'hint': 'Please run leapp with a valid locale. ' +
                        'You can get a list of installed locales by running `locale -a`.'
            })

    return pkg_repos


def get_package_repository_data():
    """ Return dictionary mapping package name with repository from which it was installed.
    Note:
        There's no yum module for py3. The dnf module can be used only on RHEL 8+,
        on RHEL 7 there's a bug in dnf preventing us to do so:
        https://bugzilla.redhat.com/show_bug.cgi?id=1789840
    """
    if not no_yum:
        return _get_package_repository_data_yum()
    if not no_dnf:
        return _get_package_repository_data_dnf()
    raise StopActorExecutionError(message=no_yum_warning_msg)


def map_modular_rpms_to_modules():
    """
    Map modular packages to the module streams they come from.
    """
    modules = module_lib.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():
            # we transform the NEVRA string into a tuple
            name, epoch_version, release_arch = rpm.rsplit('-', 2)
            epoch, version = epoch_version.split(':', 1)
            release, arch = release_arch.rsplit('.', 1)
            rpm_key = (name, epoch, version, release, arch)
            # stream could be int or float, convert it to str just in case
            rpm_streams[rpm_key] = (module.getName(), str(module.getStream()))
    return rpm_streams


# TODO(drehak) unit tests
def process():
    output = rpms.get_installed_rpms()
    pkg_repos = get_package_repository_data()
    rpm_streams = map_modular_rpms_to_modules()

    result = InstalledRPM()
    for entry in output:
        entry = entry.strip()
        if not entry:
            continue
        name, version, release, epoch, packager, arch, pgpsig = entry.split('|')
        repository = pkg_repos.get(name, '')
        rpm_key = (name, epoch, version, release, arch)
        module, stream = rpm_streams.get(rpm_key, (None, None))
        result.items.append(RPM(
            name=name,
            version=version,
            epoch=epoch,
            packager=packager,
            arch=arch,
            release=release,
            pgpsig=pgpsig,
            repository=repository,
            module=module,
            stream=stream))
    api.produce(result)


# repos/system_upgrade/common/actors/rpmscanner/tests/test_rpmscanner.py

import sys

import pytest

from leapp.libraries.actor import rpmscanner
from leapp.libraries.common import module as module_lib
from leapp.libraries.common import rpms, testutils
from leapp.libraries.stdlib import api
from leapp.models import InstalledRPM, RPM
from leapp.snactor.fixture import current_actor_context

no_yum = False
try:
    import yum
except ImportError:
    no_yum = True

no_dnf = False
try:
    import dnf
except ImportError:
    no_dnf = True


# real module streams taken from Fedora 31
ARTIFACTS_AFTERBURN = [
    'afterburn-0:4.2.0-1.module_f31+6825+8330d585.x86_64',
    'afterburn-debuginfo-0:4.2.0-1.module_f31+6825+8330d585.x86_64',
    'rust-afterburn-0:4.2.0-1.module_f31+6825+8330d585.src',
    'rust-afterburn-debugsource-0:4.2.0-1.module_f31+6825+8330d585.x86_64'
]
ARTIFACTS_SUBVERSION_110 = [
    'mod_dav_svn-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'mod_dav_svn-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'python2-subversion-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'python2-subversion-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-0:1.10.6-1.module_f31+5204+aeb0fc0d.src',
    'subversion-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-debugsource-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-devel-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-devel-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-gnome-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-gnome-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-javahl-0:1.10.6-1.module_f31+5204+aeb0fc0d.noarch',
    'subversion-kde-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-kde-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-libs-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-libs-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-perl-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-perl-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-tools-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64',
    'subversion-tools-debuginfo-0:1.10.6-1.module_f31+5204+aeb0fc0d.x86_64'
]
ARTIFACTS_SUBVERSION_113 = [
    'mod_dav_svn-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'mod_dav_svn-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'python2-subversion-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'python2-subversion-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-0:1.13.0-1.module_f31+6955+7c448939.src',
    'subversion-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-debugsource-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-devel-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-devel-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-gnome-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-gnome-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-javahl-0:1.13.0-1.module_f31+6955+7c448939.noarch',
    'subversion-kde-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-kde-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-libs-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-libs-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-perl-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-perl-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-tools-0:1.13.0-1.module_f31+6955+7c448939.x86_64',
    'subversion-tools-debuginfo-0:1.13.0-1.module_f31+6955+7c448939.x86_64'
]


class ModuleMocked(object):
    def __init__(self, name, stream, artifacts):
        self.name = name
        self.stream = stream
        self.artifacts = artifacts

    def getName(self):
        return self.name

    def getStream(self):
        return self.stream

    def getArtifacts(self):
        return self.artifacts


MODULES = [
    ModuleMocked('afterburn', 'rolling', ARTIFACTS_AFTERBURN),
    ModuleMocked('subversion', '1.10', ARTIFACTS_SUBVERSION_110),
    ModuleMocked('subversion', '1.13', ARTIFACTS_SUBVERSION_113)
]


@pytest.mark.skipif(no_yum and no_dnf, reason='yum/dnf is unavailable')
def test_actor_execution(monkeypatch, current_actor_context):
    monkeypatch.setattr(rpmscanner.module_lib, 'get_modules', lambda: [])
    current_actor_context.run()
    assert current_actor_context.consume(InstalledRPM)
    assert current_actor_context.consume(InstalledRPM)[0].items


def test_map_modular_rpms_to_modules_empty(monkeypatch):
    monkeypatch.setattr(module_lib, 'get_modules', lambda: [])
    mapping = rpmscanner.map_modular_rpms_to_modules()
    assert not mapping


def test_map_modular_rpms_to_modules(monkeypatch):
    monkeypatch.setattr(module_lib, 'get_modules', lambda: MODULES)
    mapping = rpmscanner.map_modular_rpms_to_modules()
    assert mapping[
        ('afterburn', '0', '4.2.0', '1.module_f31+6825+8330d585', 'x86_64')
    ] == ('afterburn', 'rolling')
    assert mapping[
        ('subversion', '0', '1.10.6', '1.module_f31+5204+aeb0fc0d', 'x86_64')
    ] == ('subversion', '1.10')
    assert mapping[
        ('subversion', '0', '1.13.0', '1.module_f31+6955+7c448939', 'x86_64')
    ] == ('subversion', '1.13')
    assert not mapping.get(('subversion', '0', '1.13.0', '1.module_f31+6955+7c448939', 'noarch'))
    assert not mapping.get(('subversion', '0', '1.13.1', '1.module_f31+6955+7c448939', 'x86_64'))
    assert not mapping.get(('subversion', '1', '1.13.0', '1.module_f31+6955+7c448939', 'x86_64'))


INSTALLED_RPMS = [
    ('afterburn|4.2.0|1.module_f31+6825+8330d585|0|Fedora Project|x86_64|'
     'RSA/SHA256, Wed 16 Oct 2019 12:49:08 AM CEST, Key ID 50cb390b3c3359c4'),
    ('subversion|1.10.6|1.module_f31+5204+aeb0fc0d|0|Fedora Project|x86_64|'
     'RSA/SHA256, Thu 25 Jul 2019 01:41:52 PM CEST, Key ID 50cb390b3c3359c4'),
    # non-modular, epoch
    ('tcpdump|4.9.3|2.fc31|14|Fedora Project|x86_64|'
     'RSA/SHA256, Wed 22 Jul 2020 12:25:15 PM CEST, Key ID 50cb390b3c3359c4'),
    # non-modular, no epoch
    ('passwd|0.80|7.fc31|0|Fedora Project|x86_64|'
     'RSA/SHA256, Wed 04 Dec 2019 08:48:43 PM CET, Key ID 50cb390b3c3359c4')
]


PACKAGE_REPOS = {
    'afterburn': 'repo1',
    'subversion': 'repo2',
    'tcpdump': 'repo2'
}


def test_process(monkeypatch):
    monkeypatch.setattr(module_lib, 'get_modules', lambda: MODULES)
    monkeypatch.setattr(rpmscanner, 'get_package_repository_data', lambda: PACKAGE_REPOS)
    monkeypatch.setattr(rpms, 'get_installed_rpms', lambda: INSTALLED_RPMS)
    monkeypatch.setattr(api, 'produce', testutils.produce_mocked())

    rpmscanner.process()
    assert api.produce.called
    assert len(api.produce.model_instances) == 1
    assert isinstance(api.produce.model_instances[0], InstalledRPM)
    items = {i.name: i for i in api.produce.model_instances[0].items}
    assert len(items) == 4

    assert items['afterburn'].epoch == '0'
    assert items['afterburn'].version == '4.2.0'
    assert items['afterburn'].release == '1.module_f31+6825+8330d585'
    assert items['afterburn'].arch == 'x86_64'
    assert items['afterburn'].module == 'afterburn'
    assert items['afterburn'].stream == 'rolling'

    assert items['subversion'].epoch == '0'
    assert items['subversion'].version == '1.10.6'
    assert items['subversion'].release == '1.module_f31+5204+aeb0fc0d'
    assert items['subversion'].arch == 'x86_64'
    assert items['subversion'].module == 'subversion'
    assert items['subversion'].stream == '1.10'

    assert items['tcpdump'].epoch == '14'
    assert items['tcpdump'].version == '4.9.3'
    assert items['tcpdump'].release == '2.fc31'
    assert items['tcpdump'].arch == 'x86_64'
    assert not items['tcpdump'].module
    assert not items['tcpdump'].stream

    assert items['passwd'].epoch == '0'
    assert items['passwd'].version == '0.80'
    assert items['passwd'].release == '7.fc31'
    assert items['passwd'].arch == 'x86_64'
    assert not items['passwd'].module
    assert not items['passwd'].stream


# repos/system_upgrade/common/actors/rpmtransactionconfigtaskscollector/libraries/rpmtransactionconfigtaskscollector.py

import os.path

from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM, RpmTransactionTasks


def load_tasks_file(path, logger):
    # Loads the given file and converts it to a deduplicated list of strings that are stripped
    if os.path.isfile(path):
        try:
            with open(path, 'r') as f:
                return list(
                    {entry.strip() for entry in f.read().split('\n') if entry.strip() and
                        not entry.strip().startswith('#')}
                )
        except IOError as e:
            logger.warning('Failed to open %s to load additional transaction data. Error: %s', path, str(e))
    return []


def load_tasks(base_dir, logger):
    # Loads configuration files to_install, to_keep, and to_remove from the given base directory
    rpms = next(api.consume(InstalledRedHatSignedRPM))
    rpm_names = [rpm.name for rpm in rpms.items]
    to_install = load_tasks_file(os.path.join(base_dir, 'to_install'), logger)
    # we do not want to put into rpm transaction what is already installed (it will go to "to_upgrade" bucket)
    to_install_filtered = [pkg for pkg in to_install if pkg not in rpm_names]

    filtered = set(to_install) - set(to_install_filtered)
    if filtered:
        api.current_logger().debug(
            'The following packages from "to_install" file will be ignored as they are already installed:'
            '\n- ' + '\n- '.join(filtered))

    return RpmTransactionTasks(
        to_install=to_install_filtered,
        to_keep=load_tasks_file(os.path.join(base_dir, 'to_keep'), logger),
        to_remove=load_tasks_file(os.path.join(base_dir, 'to_remove'), logger))


# repos/system_upgrade/common/actors/rpmtransactionconfigtaskscollector/tests/test_load_tasks_rpmtransactionconfigtaskscollector.py

import logging

from leapp.libraries.actor.rpmtransactionconfigtaskscollector import load_tasks, load_tasks_file
from leapp.libraries.stdlib import api
from leapp.models import InstalledRedHatSignedRPM, RPM

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


def test_load_tasks(tmpdir, monkeypatch):

    def consume_signed_rpms_mocked(*models):
        installed = [
            RPM(name='c', 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')
            ]
        yield InstalledRedHatSignedRPM(items=installed)

    monkeypatch.setattr(api, "consume", consume_signed_rpms_mocked)

    tmpdir.join('to_install').write('a\n b\n  c \n\n\nc\na\nc\nb')
    tmpdir.join('to_keep').write('a\n b\n  c \n\n\nc\na\nc\nb')
    tmpdir.join('to_remove').write('a\n b\n  c \n\n\nc\na\nc\nb')
    m = load_tasks(tmpdir.strpath, logging)
    # c is not going to be in "to_install" as it is already installed
    assert set(m.to_install) == set(['a', 'b'])
    assert set(m.to_keep) == set(['a', 'b', 'c'])
    assert set(m.to_remove) == set(['a', 'b', 'c'])


def test_load_tasks_file(tmpdir):
    f = tmpdir.join('to_install')
    f.write('a\n b\n  c \n\n\nc\na\nc\nb')
    assert set(load_tasks_file(f.strpath, logging)) == set(['a', 'b', 'c'])
    f = tmpdir.join('to_keep')
    f.write(' ')
    assert set(load_tasks_file(f.strpath, logging)) == set([])


# repos/system_upgrade/common/actors/scanclienablerepo/libraries/scanclienablerepo.py

from leapp.libraries.common import config
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepository


def process():
    if not config.get_env('LEAPP_ENABLE_REPOS'):
        return
    api.current_logger().info('The --enablerepo option has been used,')
    for repoid in config.get_env('LEAPP_ENABLE_REPOS').split(','):
        api.produce(CustomTargetRepository(repoid=repoid))


# repos/system_upgrade/common/actors/scanclienablerepo/tests/test_unit_scanclienablerepo.py

import pytest

from leapp.libraries.actor import scanclienablerepo
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepository


class LoggerMocked(object):
    def __init__(self):
        self.infomsg = None
        self.debugmsg = None

    def info(self, msg):
        self.infomsg = msg

    def debug(self, msg):
        self.debugmsg = msg

    def __call__(self):
        return self


def test_no_enabledrepos(monkeypatch):
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', LoggerMocked())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())
    scanclienablerepo.process()
    assert not api.current_logger.infomsg
    assert not api.produce.called


@pytest.mark.parametrize('envars,result', [
    ({'LEAPP_ENABLE_REPOS': 'repo1'}, [CustomTargetRepository(repoid='repo1')]),
    ({'LEAPP_ENABLE_REPOS': 'repo1,repo2'}, [CustomTargetRepository(repoid='repo1'),
                                             CustomTargetRepository(repoid='repo2')]),
])
def test_enabledrepos(monkeypatch, envars, result):
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', LoggerMocked())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(envars=envars))
    scanclienablerepo.process()
    assert api.current_logger.infomsg
    assert api.produce.called == len(result)
    for i in result:
        assert i in api.produce.model_instances


# repos/system_upgrade/common/actors/scancpu/libraries/scancpu.py

import re

from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import CPUInfo, DetectedDeviceOrDriver, DeviceDriverDeprecationData

LSCPU_NAME_VALUE = re.compile(r'(?P<name>[^:]+):\s+(?P<value>.+)\n?')
PPC64LE_MODEL = re.compile(r'\d+\.\d+ \(pvr (?P<family>[0-9a-fA-F]+) 0*[0-9a-fA-F]+\)')


def _get_lscpu_output():
    try:
        result = run(['lscpu'])
        return result.get('stdout', '')
    except (OSError, CalledProcessError):
        api.current_logger().debug('Executing `lscpu` failed', exc_info=True)
    return ''


def _get_cpu_entries_for(arch_prefix):
    result = []
    for message in api.consume(DeviceDriverDeprecationData):
        result.extend([
            entry for entry in message.entries
            if entry.device_type == 'cpu' and entry.device_id.startswith(arch_prefix)
        ])
    return result


def _is_detected_aarch64(lscpu, entry):
    # Currently not applicable - We don't have a way to correctly detect this
    # But we should find a way to properly do so in future.

    # Shut up warnings:
    entry = lscpu
    lscpu = entry
    return False


def _is_detected_s390x(lscpu, entry):
    try:
        _, _, machine_type, _ = entry.device_id.split(':', 4)
        return machine_type == lscpu.get('Machine type')
    except ValueError:
        return False


def _is_detected_ppc64le(lscpu, entry):
    try:
        _, _, machine_type, _ = entry.device_id.split(':', 4)
        match = PPC64LE_MODEL.match(lscpu.get('Model'))
        if not match:
            return False
        family = match.group('family')
        return family and machine_type.lstrip('0').lower() == family.lstrip('0').lower()
    except ValueError:
        return False


def _make_set(value):
    """
    Converts range/set specification to a concrete set of numbers

    '[1-3]'         => {1, 2, 3}
    '{1,2,3}'       => {1, 2, 3}
    '{[1-3]}        => {1, 2, 3}
    '{[1-3],[5-7]}  => {1, 2, 3, 5, 6, 7}
    """
    result = set()
    for vrange in value.strip('{} ').split(','):
        if '[' not in vrange:
            try:
                result.add(int(vrange))
            except ValueError:
                pass
        else:
            try:
                start, end = vrange.strip('[] ').split('-')
                result.update(range(int(start.strip()), int(end.strip()) + 1))
            except ValueError:
                pass
    return result


def _match_model(needle, hay):
    try:
        ineedle = int(needle)
    except ValueError:
        return False
    if not hay or hay == '*':
        # Match all
        return True
    if '[' not in hay and '{' not in hay:
        return hay == needle
    return ineedle in _make_set(hay)


def _is_detected_x86_64(lscpu, entry):
    vendors = {'amd': 'AuthenticAMD', 'intel': 'GenuineIntel'}
    try:
        _, vendor, family, model = entry.device_id.split(':', 4)
        if vendor in vendors and vendors[vendor] == lscpu.get('Vendor ID'):
            if family == lscpu.get('CPU family'):
                return _match_model(lscpu.get('Model'), model)
        return False
    except ValueError:
        return False


def _to_detected_device(entry):
    return DetectedDeviceOrDriver(**entry.dump())


def _find_deprecation_data_entries(lscpu):
    arch_prefix, is_detected = None, None
    if architecture.matches_architecture(architecture.ARCH_X86_64):
        arch_prefix, is_detected = architecture.ARCH_X86_64, _is_detected_x86_64
    elif architecture.matches_architecture(architecture.ARCH_PPC64LE):
        arch_prefix, is_detected = architecture.ARCH_PPC64LE, _is_detected_ppc64le
    elif architecture.matches_architecture(architecture.ARCH_S390X):
        arch_prefix, is_detected = architecture.ARCH_S390X, _is_detected_s390x
    elif architecture.matches_architecture(architecture.ARCH_ARM64):
        arch_prefix, is_detected = architecture.ARCH_ARM64, _is_detected_aarch64

    if arch_prefix and is_detected:
        return [
            _to_detected_device(entry) for entry in _get_cpu_entries_for(arch_prefix)
            if is_detected(lscpu, entry)
        ]

    api.current_logger().warn('Unsupported platform could not detect relevant CPU information')
    return []


def process():
    lscpu = dict(LSCPU_NAME_VALUE.findall(_get_lscpu_output()))
    api.produce(*_find_deprecation_data_entries(lscpu))
    # Backwards compatibility
    machine_type = lscpu.get('Machine type')
    api.produce(CPUInfo(machine_type=int(machine_type) if machine_type else None))


# repos/system_upgrade/common/actors/scancpu/tests/test_scancpu.py

import os

from leapp.libraries.actor import scancpu
from leapp.libraries.common import testutils
from leapp.libraries.stdlib import api
from leapp.models import CPUInfo

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


class mocked_get_cpuinfo(object):
    def __init__(self, filename):
        self.filename = filename

    def __call__(self):
        """
        Return lines of the self.filename test file located in the files directory.

        Those files contain /proc/cpuinfo content from several machines.
        """
        with open(os.path.join(CUR_DIR, 'files', self.filename), 'r') as fp:
            return '\n'.join(fp.read().splitlines())


def test_machine_type(monkeypatch):

    # cpuinfo doesn't contain a machine field
    mocked_cpuinfo = mocked_get_cpuinfo('lscpu_x86_64')
    monkeypatch.setattr(scancpu, '_get_lscpu_output', mocked_cpuinfo)
    monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
    current_actor = testutils.CurrentActorMocked(arch=testutils.architecture.ARCH_X86_64)
    monkeypatch.setattr(api, 'current_actor', current_actor)
    scancpu.process()
    assert api.produce.called == 1
    assert CPUInfo() == api.produce.model_instances[0]

    # cpuinfo contains a machine field
    api.produce.called = 0
    api.produce.model_instances = []
    current_actor = testutils.CurrentActorMocked(arch=testutils.architecture.ARCH_S390X)
    monkeypatch.setattr(api, 'current_actor', current_actor)
    mocked_cpuinfo.filename = 'lscpu_s390x'
    scancpu.process()
    assert api.produce.called == 1
    assert CPUInfo(machine_type=2827) == api.produce.model_instances[0]


# repos/system_upgrade/common/actors/scancustomrepofile/libraries/scancustomrepofile.py

import os

from leapp.libraries.common import repofileutils
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepository, CustomTargetRepositoryFile

CUSTOM_REPO_PATH = "/etc/leapp/files/leapp_upgrade_repositories.repo"


def process():
    """
    Produce CustomTargetRepository msgs for the custom repo file if the file
    exists.

    The CustomTargetRepository msg is produced for every repository inside
    the <CUSTOM_REPO_PATH> file.
    """
    if not os.path.isfile(CUSTOM_REPO_PATH):
        api.current_logger().debug(
                "The {} file doesn't exist. Nothing to do."
                .format(CUSTOM_REPO_PATH))
        return
    api.current_logger().info("The {} file exists.".format(CUSTOM_REPO_PATH))
    repofile = repofileutils.parse_repofile(CUSTOM_REPO_PATH)
    if not repofile.data:
        return
    api.produce(CustomTargetRepositoryFile(file=CUSTOM_REPO_PATH))
    for repo in repofile.data:
        api.produce(CustomTargetRepository(
            repoid=repo.repoid,
            name=repo.name,
            baseurl=repo.baseurl,
            enabled=repo.enabled,
        ))


# repos/system_upgrade/common/actors/scancustomrepofile/tests/test_scancustomrepofile.py

import os

from leapp.libraries.actor import scancustomrepofile
from leapp.libraries.common import repofileutils
from leapp.libraries.common.testutils import produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import CustomTargetRepository, CustomTargetRepositoryFile, RepositoryData, RepositoryFile

_REPODATA = [
    RepositoryData(repoid="repo1", name="repo1name", baseurl="repo1url", enabled=True),
    RepositoryData(repoid="repo2", name="repo2name", baseurl="repo2url", enabled=False),
    RepositoryData(repoid="repo3", name="repo3name", enabled=True),
    RepositoryData(repoid="repo4", name="repo4name", mirrorlist="mirror4list", enabled=True),
]

_CUSTOM_REPOS = [
    CustomTargetRepository(repoid="repo1", name="repo1name", baseurl="repo1url", enabled=True),
    CustomTargetRepository(repoid="repo2", name="repo2name", baseurl="repo2url", enabled=False),
    CustomTargetRepository(repoid="repo3", name="repo3name", baseurl=None, enabled=True),
    CustomTargetRepository(repoid="repo4", name="repo4name", baseurl=None, enabled=True),
]

_CUSTOM_REPO_FILE_MSG = CustomTargetRepositoryFile(file=scancustomrepofile.CUSTOM_REPO_PATH)


class LoggerMocked(object):
    def __init__(self):
        self.infomsg = None
        self.debugmsg = None

    def info(self, msg):
        self.infomsg = msg

    def debug(self, msg):
        self.debugmsg = msg

    def __call__(self):
        return self


def test_no_repofile(monkeypatch):
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: False)
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', LoggerMocked())
    scancustomrepofile.process()
    msg = "The {} file doesn't exist. Nothing to do.".format(scancustomrepofile.CUSTOM_REPO_PATH)
    assert api.current_logger.debugmsg == msg
    assert not api.produce.called


def test_valid_repofile_exists(monkeypatch):
    def _mocked_parse_repofile(fpath):
        return RepositoryFile(file=fpath, data=_REPODATA)
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: True)
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(repofileutils, 'parse_repofile', _mocked_parse_repofile)
    monkeypatch.setattr(api, 'current_logger', LoggerMocked())
    scancustomrepofile.process()
    msg = "The {} file exists.".format(scancustomrepofile.CUSTOM_REPO_PATH)
    assert api.current_logger.infomsg == msg
    assert api.produce.called == len(_CUSTOM_REPOS) + 1
    assert _CUSTOM_REPO_FILE_MSG in api.produce.model_instances
    for crepo in _CUSTOM_REPOS:
        assert crepo in api.produce.model_instances


def test_empty_repofile_exists(monkeypatch):
    def _mocked_parse_repofile(fpath):
        return RepositoryFile(file=fpath, data=[])
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: True)
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(repofileutils, 'parse_repofile', _mocked_parse_repofile)
    monkeypatch.setattr(api, 'current_logger', LoggerMocked())
    scancustomrepofile.process()
    msg = "The {} file exists.".format(scancustomrepofile.CUSTOM_REPO_PATH)
    assert api.current_logger.infomsg == msg
    assert not api.produce.called


# repos/system_upgrade/common/actors/scandasd/libraries/scandasd.py

import os

from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import CopyFile, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks

DASD_CONF = '/etc/dasd.conf'


def process():
    if not architecture.matches_architecture(architecture.ARCH_S390X):
        return
    copy_files = []
    if os.path.isfile(DASD_CONF):
        # the file has to be copied into the targetuserspace container first,
        # then it can be included into the initramfs ==> both messages are
        # needed to be produced
        copy_files = [CopyFile(src=DASD_CONF)]
        api.produce(UpgradeInitramfsTasks(include_files=[DASD_CONF]))
    else:
        api.current_logger().info(
            "The {} file has not been discovered. DASD not used."
            .format(DASD_CONF)
        )
    api.produce(TargetUserSpaceUpgradeTasks(copy_files=copy_files, install_rpms=['s390utils-core']))


# repos/system_upgrade/common/actors/scandasd/tests/unit_test_scandasd.py

import os

import pytest

from leapp.libraries.actor import scandasd
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
from leapp.models import CopyFile, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks


def test_dasd_exists(monkeypatch):
    monkeypatch.setattr(scandasd.api, 'current_actor', CurrentActorMocked(arch=architecture.ARCH_S390X))
    monkeypatch.setattr(scandasd.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(scandasd.api, 'produce', produce_mocked())
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: True)
    scandasd.process()
    assert not scandasd.api.current_logger.infomsg
    assert scandasd.api.produce.called == 2
    tusut_flag = False
    uit_flag = False
    for msg in scandasd.api.produce.model_instances:
        if isinstance(msg, TargetUserSpaceUpgradeTasks):
            assert [CopyFile(src=scandasd.DASD_CONF)] == msg.copy_files
            assert msg.install_rpms == ['s390utils-core']
            tusut_flag = True
        elif isinstance(msg, UpgradeInitramfsTasks):
            assert [scandasd.DASD_CONF] == msg.include_files
            uit_flag = True
    assert tusut_flag and uit_flag


def test_dasd_not_found(monkeypatch):
    monkeypatch.setattr(scandasd.api, 'current_actor', CurrentActorMocked(arch=architecture.ARCH_S390X))
    monkeypatch.setattr(scandasd.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: False)
    monkeypatch.setattr(scandasd.api, 'produce', produce_mocked())
    scandasd.process()
    assert scandasd.api.current_logger.infomsg
    assert scandasd.api.produce.called == 1
    assert len(scandasd.api.produce.model_instances) == 1
    assert isinstance(scandasd.api.produce.model_instances[0], TargetUserSpaceUpgradeTasks)
    assert scandasd.api.produce.model_instances[0].install_rpms == ['s390utils-core']
    assert not scandasd.api.produce.model_instances[0].copy_files


@pytest.mark.parametrize('isfile', [True, False])
@pytest.mark.parametrize('arch', [
    architecture.ARCH_X86_64,
    architecture.ARCH_ARM64,
    architecture.ARCH_PPC64LE,
])
def test_non_ibmz_arch(monkeypatch, isfile, arch):
    monkeypatch.setattr(scandasd.api, 'current_actor', CurrentActorMocked(arch=arch))
    monkeypatch.setattr(scandasd.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(scandasd.api, 'produce', produce_mocked())
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: isfile)
    scandasd.process()
    assert not scandasd.api.current_logger.infomsg
    assert not scandasd.api.produce.called


# repos/system_upgrade/common/actors/scanfilesfortargetuserspace/libraries/scanfilesfortargetuserspace.py

import os

from leapp.libraries.stdlib import api
from leapp.models import CopyFile, TargetUserSpacePreupgradeTasks

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


def scan_files_to_copy():
    """
    Scans the source system and identifies files that should be copied into target userspace.

    When an item to be copied is identified a message :class:`CopyFile` is produced.
    """
    files_to_copy = []
    for src_path in FILES_TO_COPY_IF_PRESENT:
        if os.path.isfile(src_path):
            dst_path = FILES_TO_COPY_IF_PRESENT[src_path]
            files_to_copy.append(CopyFile(src=src_path, dst=dst_path))

    preupgrade_task = TargetUserSpacePreupgradeTasks(copy_files=files_to_copy)

    api.produce(preupgrade_task)


# repos/system_upgrade/common/actors/scanfilesfortargetuserspace/tests/test_scanfilesfortargetuserspace.py

import os

import pytest

from leapp.libraries.actor.scanfilesfortargetuserspace import scan_files_to_copy
from leapp.libraries.common.testutils import produce_mocked
from leapp.libraries.stdlib import api


@pytest.fixture
def isfile_default_config():
    config = {
        '/etc/hosts': True
    }
    return config


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


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

    The created function raises :class:`ValueError` should the unit under test try to "isfile"
    a path that is not present in the configuration.

    One global mocked function with configuration is error prone as individual test would
    have to return the configuration into the original state after execution.
    """

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


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

    scan_files_to_copy()

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

    preupgrade_task_msg = actor_produces.model_instances[0]

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

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

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


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

    monkeypatch.setattr(os.path, 'isfile', mocked_isfile)
    monkeypatch.setattr(api, 'produce', actor_produces)

    scan_files_to_copy()

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

    preupgrade_task_msg = actor_produces.model_instances[0]
    should_copy_hostsfile = do_files_to_copy_contain_entry_with_src(preupgrade_task_msg.copy_files, '/etc/hosts')
    assert not should_copy_hostsfile, 'Identified /etc/hosts as a file to be copied even if it doesn\'t exists'

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


# repos/system_upgrade/common/actors/scaninstalledtargetkernelversion/libraries/scankernel.py

from leapp.libraries.common.config.version import get_target_major_version, is_rhel_realtime
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import InstalledTargetKernelVersion


def _get_kernel_version(kernel_name):
    try:
        kernels = run(['rpm', '-q', kernel_name], split=True)['stdout']
    except CalledProcessError:
        return ''

    for kernel in kernels:
        # name-version-release - we want the last two fields only
        version = '-'.join(kernel.split('-')[-2:])
        if 'el{}'.format(get_target_major_version()) in version:
            return version
    return ''


def process():
    # pylint: disable=no-else-return  - false positive
    # TODO: should we take care about stuff of kernel-rt and kernel in the same
    # time when both are present? or just one? currently, handle only one
    # of these during the upgrade. kernel-rt has higher prio when original sys
    # was realtime

    if is_rhel_realtime():
        version = _get_kernel_version('kernel-rt')
        if version:
            api.produce(InstalledTargetKernelVersion(version=version))
            return
        else:
            api.current_logger().warning(
                'The kernel-rt rpm from the target RHEL has not been detected. '
                'Switching to non-preemptive kernel.'
            )
            # TODO: create report with instructions to install kernel-rt manually
            # - attach link to article if any
            # - this possibly happens just in case the repository with kernel-rt
            # # is not enabled during the upgrade.

    # standard (non-preemptive) kernel
    version = _get_kernel_version('kernel')
    if version:
        api.produce(InstalledTargetKernelVersion(version=version))
    else:
        # This is very unexpected situation. At least one kernel has to be
        # installed always. Some actors consuming the InstalledTargetKernelVersion
        # will crash without the created message. I am keeping kind of original
        # behaviour in this case, but at least the let me log the error msg
        #
        api.current_logger().error('Cannot detect any kernel RPM')
        # StopActorExecutionError('Cannot detect any target RHEL kernel RPM.')


# repos/system_upgrade/common/actors/scaninstalledtargetkernelversion/tests/test_scaninstalledkernel_scaninstalledtargetkernelversion.py

import pytest

from leapp.libraries import stdlib
from leapp.libraries.actor import scankernel
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked
from leapp.libraries.stdlib import api

TARGET_KERNEL_VERSION = '1.2.3-4.el8.x86_64'
TARGET_RT_KERNEL_VERSION = '1.2.3-4.rt56.7.el8.x86_64'
TARGET_KERNEL = 'kernel-{}'.format(TARGET_KERNEL_VERSION)
TARGET_RT_KERNEL = 'kernel-{}'.format(TARGET_RT_KERNEL_VERSION)
OLD_KERNEL = 'kernel-0.1.2-3.el7.x86_64'
OLD_RT_KERNEL = 'kernel-rt-0.1.2-3.rt4.5.el7.x86_64'


class MockedRun(object):

    def __init__(self, stdouts):
        # stdouts should be dict of list of strings: { str: [str1,str2,...]}
        self._stdouts = stdouts

    def __call__(self, *args, **kwargs):
        for key in ('kernel', 'kernel-rt'):
            if key in args[0]:
                return {'stdout': self._stdouts.get(key, [])}
        return {'stdout': []}


@pytest.mark.parametrize('is_rt,exp_version,stdouts', [
    (False, TARGET_KERNEL_VERSION, {'kernel': [OLD_KERNEL, TARGET_KERNEL]}),
    (False, TARGET_KERNEL_VERSION, {'kernel': [TARGET_KERNEL, OLD_KERNEL]}),
    (False, TARGET_KERNEL_VERSION, {
        'kernel': [TARGET_KERNEL, OLD_KERNEL],
        'kernel-rt': [TARGET_RT_KERNEL, OLD_RT_KERNEL],
    }),
    (True, TARGET_RT_KERNEL_VERSION, {'kernel-rt': [OLD_RT_KERNEL, TARGET_RT_KERNEL]}),
    (True, TARGET_RT_KERNEL_VERSION, {'kernel-rt': [TARGET_RT_KERNEL, OLD_RT_KERNEL]}),
    (True, TARGET_RT_KERNEL_VERSION, {
        'kernel': [TARGET_KERNEL, OLD_KERNEL],
        'kernel-rt': [TARGET_RT_KERNEL, OLD_RT_KERNEL],
    }),
])
def test_scaninstalledkernel(monkeypatch, is_rt, exp_version, stdouts):
    result = []
    old_kver = '0.1.2-3.rt4.5.el7.x86_64' if is_rt else 'kernel-0.1.2-3.el7.x86_64'
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(kernel=old_kver))
    monkeypatch.setattr(api, 'produce', result.append)
    monkeypatch.setattr(scankernel, 'run', MockedRun(stdouts))
    scankernel.process()
    assert len(result) == 1 and result[0].version == exp_version


def test_scaninstalledkernel_missing_rt(monkeypatch):
    result = []
    old_kver = '0.1.2-3.rt4.5.el7.x86_64'
    stdouts = {'kernel': [TARGET_KERNEL], 'kernel-rt': [OLD_RT_KERNEL]}
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(kernel=old_kver))
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(api, 'produce', result.append)
    monkeypatch.setattr(scankernel, 'run', MockedRun(stdouts))
    scankernel.process()
    assert api.current_logger.warnmsg
    assert len(result) == 1 and result[0].version == TARGET_KERNEL_VERSION


def test_scaninstalledkernel_missing(monkeypatch):
    result = []
    old_kver = '0.1.2-3.rt4.5.el7.x86_64'
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(kernel=old_kver))
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(api, 'produce', result.append)
    monkeypatch.setattr(scankernel, 'run', MockedRun({}))
    scankernel.process()
    assert api.current_logger.warnmsg
    assert api.current_logger.errmsg
    assert not result


# repos/system_upgrade/common/actors/scanmemory/libraries/scanmemory.py

from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp.models import MemoryInfo


def _get_memoryinfo(filename='/proc/meminfo'):
    """ Returns dict of all memory information from /proc/meminfo file """

    try:
        with open(filename) as fp:
            # format of the lines: Key:  value unit
            # e.g.:                MemTotal:   1024 kB
            return dict((i.split()[0].rstrip(':'), int(i.split()[1])) for i in fp.readlines())
    except IOError:
        raise StopActorExecutionError(
            'Could not read memory values',
            details={'details': 'Error opening file {}'.format(filename)},
        )


def process():
    mem_info = _get_memoryinfo()
    if mem_info:
        api.produce(MemoryInfo(mem_total=mem_info['MemTotal']))


# repos/system_upgrade/common/actors/scanmemory/tests/test_scanmemory.py

import mock
import six

from leapp.libraries.actor import scanmemory
from leapp.libraries.common import testutils
from leapp.libraries.stdlib import api
from leapp.models import MemoryInfo


def test_with_low_memory(monkeypatch):
    if six.PY3:
        with mock.patch("builtins.open", mock.mock_open(read_data="MemTotal: 42 kB")) as mock_proc_meminfo:
            monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
            scanmemory.process()
            mock_proc_meminfo.assert_called_once_with('/proc/meminfo')
            assert api.produce.called == 1
            assert MemoryInfo(mem_total=42) == api.produce.model_instances[0]
    else:
        with mock.patch("__builtin__.open", mock.mock_open(read_data="MemTotal: 42 kB")) as mock_proc_meminfo:
            monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
            scanmemory.process()
            mock_proc_meminfo.assert_called_once_with('/proc/meminfo')
            assert api.produce.called == 1
            assert MemoryInfo(mem_total=42) == api.produce.model_instances[0]


def test_with_high_memory(monkeypatch):
    if six.PY3:
        with mock.patch("builtins.open", mock.mock_open(read_data="MemTotal: 424242424242 kB")) as mock_proc_meminfo:
            monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
            scanmemory.process()
            mock_proc_meminfo.assert_called_once_with('/proc/meminfo')
            assert api.produce.called == 1
            assert MemoryInfo(mem_total=424242424242) == api.produce.model_instances[0]
    else:
        with mock.patch(
            "__builtin__.open",
            mock.mock_open(read_data="MemTotal: 424242424242 kB"),
        ) as mock_proc_meminfo:
            monkeypatch.setattr(api, 'produce', testutils.produce_mocked())
            scanmemory.process()
            mock_proc_meminfo.assert_called_once_with('/proc/meminfo')
            assert api.produce.called == 1
            assert MemoryInfo(mem_total=424242424242) == api.produce.model_instances[0]


# repos/system_upgrade/common/actors/scanpkgmanager/libraries/pluginscanner.py

import re

from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import run

# When the output spans multiple lines, each of the lines after the first one
# start with a '    <SPACES>    : '
LOADED_PLUGINS_NEXT_LINE_START = ' +: '


def _parse_loaded_plugins(package_manager_output):
    """
    Retrieves a list of plugins that are being loaded when calling dnf/yum.

    :param dict package_manager_output: The result of running the package manager command.
    :rtype: list
    :returns: A list of plugins that are being loaded by the package manager.
    """
    # Package manager might break the information about loaded plugins into multiple lines,
    # we need to concaternate the list ourselves
    loaded_plugins_str = ''
    for line in package_manager_output['stdout']:
        if line.startswith('Loaded plugins:'):
            # We have found the first line that contains the plugins
            plugins_on_this_line = line[16:]  # Remove the `Loaded plugins: ` part

            if plugins_on_this_line[-1] == ',':
                plugins_on_this_line += ' '

            loaded_plugins_str += plugins_on_this_line
            continue

        if loaded_plugins_str:
            if re.match(LOADED_PLUGINS_NEXT_LINE_START, line):
                # The list of plugins continues on this line
                plugins_on_this_line = line.lstrip(' :')  # Remove the leading spaces and semicolon

                # Plugins are separated by ', ', however the space at the end of line might get dropped, add it
                # so we can split it by ', ' later
                if plugins_on_this_line[-1] == ',':
                    plugins_on_this_line += ' '

                loaded_plugins_str += plugins_on_this_line
            else:
                # The list of loaded plugins ended
                break

    return loaded_plugins_str.split(', ')


def scan_enabled_package_manager_plugins():
    """
    Runs package manager (yum/dnf) command and parses its output for enabled/loaded plugins.

    :return: A list of enabled plugins.
    :rtype: List
    """

    # We rely on package manager itself to report what plugins are used when it is invoked.
    # An alternative approach would be to check the install path for package manager plugins
    # and parse corresponding plugin configuration files.

    if get_source_major_version() == '7':
        # in case of yum, set debuglevel=2 to be sure the output is always
        # same. The format of data is different for various debuglevels
        cmd = ['yum', '--setopt=debuglevel=2']
    else:
        # the verbose mode in dnf always set particular debuglevel, so the
        # output is not affected by the default debug level set on the
        # system
        cmd = ['dnf', '-v']  # On RHEL8 we need to supply an extra switch

    pkg_manager_output = run(cmd, split=True, checked=False)  # The command will certainly fail (does not matter).

    return _parse_loaded_plugins(pkg_manager_output)


# repos/system_upgrade/common/actors/scanpkgmanager/libraries/scanpkgmanager.py

import os
import re

from leapp.libraries.actor import pluginscanner
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import api
from leapp.models import PkgManagerInfo

YUM_CONFIG_PATH = '/etc/yum.conf'
DNF_CONFIG_PATH = '/etc/dnf/dnf.conf'


def _get_releasever_path():
    default_manager = 'yum' if get_source_major_version() == '7' else 'dnf'
    return '/etc/{}/vars/releasever'.format(default_manager)


def _releasever_exists(releasever_path):
    return os.path.isfile(releasever_path)


def get_etc_releasever():
    """ Get release version from "/etc/{yum,dnf}/vars/releasever" file """

    releasever_path = _get_releasever_path()
    if not _releasever_exists(releasever_path):
        return None

    with open(releasever_path, 'r') as fo:
        # we care about the first line only
        releasever = fo.readline().strip()

    return releasever


def _get_config_contents(config_path):
    if os.path.isfile(config_path):
        with open(config_path, 'r') as config:
            return config.read()
    return ''


def _get_proxy_if_set(manager_config_path):
    """
    Get proxy address from specified package manager config.

    :param str manager_config_path: path to a package manager config
    :returns: proxy address or None when not set
    :rtype: str
    """

    config = _get_config_contents(manager_config_path)

    for line in config.split('\n'):
        if re.match('^proxy[ \t]*=', line):
            proxy_address = line.split('=', 1)[1]
            return proxy_address.strip()

    return None


def get_configured_proxies():
    """
    Get a list of proxies used in dnf and yum configuration files.

    :returns: sorted list of unique proxies
    :rtype: List
    """

    configured_proxies = set()
    for config_path in (DNF_CONFIG_PATH, YUM_CONFIG_PATH):
        proxy = _get_proxy_if_set(config_path)
        if proxy:
            configured_proxies.add(proxy)

    return sorted(configured_proxies)


def process():
    pkg_manager_info = PkgManagerInfo()
    pkg_manager_info.etc_releasever = get_etc_releasever()
    pkg_manager_info.configured_proxies = get_configured_proxies()
    pkg_manager_info.enabled_plugins = pluginscanner.scan_enabled_package_manager_plugins()

    api.produce(pkg_manager_info)


# repos/system_upgrade/common/actors/scanpkgmanager/tests/test_pluginscanner.py

import pytest

from leapp.libraries.actor import pluginscanner

CMD_YUM_OUTPUT = '''Loaded plugins: langpacks, my plugin, subscription-manager, product-id
Usage: yum [options] COMMAND
'''
CMD_YUM_OUTPUT_MULTILINE_BREAK_ON_HYPHEN = '''Loaded plugins: langpacks, my plugin, subscription-
              : manager, product-id
Usage: yum [options] COMMAND
'''
CMD_YUM_OUTPUT_MULTILINE_BREAK_ON_WHITESPACE = '''Loaded plugins: langpacks, my plugin,
              : subscription-manager, product-id
Usage: yum [options] COMMAND
'''


def assert_plugins_identified_as_enabled(expected_plugins, identified_plugins):
    fail_description = 'Failed to parse a plugin from the package manager output.'
    for expected_enabled_plugin in expected_plugins:
        assert expected_enabled_plugin in identified_plugins, fail_description


@pytest.mark.parametrize(
    ('source_major_version', 'command'),
    [
        ('7', ['yum', '--setopt=debuglevel=2']),
        ('8', ['dnf', '-v']),
    ]
)
def test_scan_enabled_plugins(monkeypatch, source_major_version, command):
    """Tests whether the enabled plugins are correctly retrieved from the package manager output."""

    def run_mocked(cmd, **kwargs):
        if cmd == command:
            return {
                'stdout': CMD_YUM_OUTPUT.split('\n'),
                'stderr': 'You need to give some command',
                'exit_code': 1
            }
        raise ValueError('Tried to run an unexpected command.')

    def get_source_major_version_mocked():
        return source_major_version

    # The library imports `run` all the way into its namespace (from ...stdlib import run),
    # we must overwrite it there then:
    monkeypatch.setattr(pluginscanner, 'run', run_mocked)
    monkeypatch.setattr(pluginscanner, 'get_source_major_version', get_source_major_version_mocked)

    enabled_plugins = pluginscanner.scan_enabled_package_manager_plugins()
    assert_plugins_identified_as_enabled(
        ['langpacks', 'my plugin', 'subscription-manager', 'product-id'],
        enabled_plugins
    )


@pytest.mark.parametrize(
    ('yum_output',),
    [
        (CMD_YUM_OUTPUT,),
        (CMD_YUM_OUTPUT_MULTILINE_BREAK_ON_HYPHEN,),
        (CMD_YUM_OUTPUT_MULTILINE_BREAK_ON_WHITESPACE,)
    ])
def test_yum_loaded_plugins_multiline_output(yum_output, monkeypatch):
    """Tests whether the library correctly handles plugins getting reported on multiple lines."""
    def run_mocked(cmd, **kwargs):
        return {
            'stdout': yum_output.split('\n'),
            'stderr': 'You need to give some command',
            'exit_code': 1
        }

    monkeypatch.setattr(pluginscanner, 'run', run_mocked)
    monkeypatch.setattr(pluginscanner, 'get_source_major_version', lambda: '7')

    enabled_plugins = pluginscanner.scan_enabled_package_manager_plugins()

    assert len(enabled_plugins) == 4, 'Identified more yum plugins than available in the mocked yum output.'
    assert_plugins_identified_as_enabled(
        ['langpacks', 'my plugin', 'subscription-manager', 'product-id'],
        enabled_plugins
    )


# repos/system_upgrade/common/actors/scanpkgmanager/tests/test_scanpkgmanager.py

import os

import pytest

from leapp.libraries import stdlib
from leapp.libraries.actor import pluginscanner, scanpkgmanager
from leapp.libraries.common import testutils
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api

CUR_DIR = os.path.dirname(os.path.abspath(__file__))
PROXY_ADDRESS = 'https://192.168.121.123:3128'
YUM_CONFIG_PATH = '/etc/yum.conf'
DNF_CONFIG_PATH = '/etc/dnf/dnf.conf'


def mock_releasever_exists(overrides):
    def mocked_releasever_exists(name):
        if name in overrides:
            return overrides[name]
        raise ValueError
    return mocked_releasever_exists


def mocked_get_releasever_path():
    return os.path.join(CUR_DIR, 'files/releasever')


@pytest.mark.parametrize('etcrelease_exists', [True, False])
def test_get_etcreleasever(monkeypatch, etcrelease_exists):
    monkeypatch.setattr(
        scanpkgmanager,
        '_releasever_exists', mock_releasever_exists(
            {
                os.path.join(CUR_DIR, 'files/releasever'): etcrelease_exists,
            }
        )
    )
    monkeypatch.setattr(scanpkgmanager.api, 'produce', produce_mocked())
    monkeypatch.setattr(scanpkgmanager.api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(scanpkgmanager, '_get_releasever_path', mocked_get_releasever_path)
    monkeypatch.setattr(scanpkgmanager, '_get_proxy_if_set', lambda x: None)
    monkeypatch.setattr(pluginscanner, 'scan_enabled_package_manager_plugins', lambda: [])

    scanpkgmanager.process()

    assert scanpkgmanager.api.produce.called
    if etcrelease_exists:
        assert api.produce.model_instances[0].etc_releasever
    else:
        assert not api.produce.model_instances[0].etc_releasever


@pytest.mark.parametrize('proxy_set', [True, False])
def test_get_proxy_if_set(monkeypatch, proxy_set):

    config_path = '/path/to/config.conf'
    config_contents = '[main]\n'
    if proxy_set:
        config_contents += 'proxy = \t{} '.format(PROXY_ADDRESS)

    def mocked_get_config_contents(path):
        assert path == config_path
        return config_contents

    monkeypatch.setattr(scanpkgmanager, '_get_config_contents', mocked_get_config_contents)

    proxy = scanpkgmanager._get_proxy_if_set(config_path)

    if proxy_set:
        assert proxy == PROXY_ADDRESS

    assert proxy_set == bool(proxy)


@pytest.mark.parametrize(
    ('proxy_set_in_dnf_config', 'proxy_set_in_yum_config', 'expected_output'),
    [
      (True, True, [PROXY_ADDRESS]),
      (True, False, [PROXY_ADDRESS]),
      (False, False, [])
    ]
)
def test_get_configured_proxies(monkeypatch, proxy_set_in_dnf_config, proxy_set_in_yum_config, expected_output):

    def mocked_get_proxy_if_set(path):
        proxy = PROXY_ADDRESS if proxy_set_in_yum_config else None
        if path == DNF_CONFIG_PATH:
            proxy = PROXY_ADDRESS if proxy_set_in_dnf_config else None
        return proxy

    monkeypatch.setattr(scanpkgmanager, '_get_proxy_if_set', mocked_get_proxy_if_set)

    configured_proxies = scanpkgmanager.get_configured_proxies()
    assert configured_proxies == expected_output


# repos/system_upgrade/common/actors/scansaphana/libraries/scansaphana.py

from os import listdir, path

from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import SapHanaInfo, SapHanaInstanceInfo, SapHanaManifestEntry

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


def perform_sap_hana_scan():
    """
    Produces a message with details collected around SAP HANA.
    """

    api.produce(search_sap_hana_instances())


def parse_manifest(path):
    """ Parses a SAP HANA manifest into a dictionary """
    def _decoded(s):
        """
        Compatibility between Python2 and 3 - Python 2 has no str.decode but we can process str directly.
        Python3 needs the byte data to be decoded so we can process the string data.
        """
        if hasattr(s, 'decode'):
            return s.decode('utf-8')
        return s

    data = []
    try:
        with open(path, 'r') as f:
            for line in _decoded(f.read()).split('\n'):
                try:
                    key, value = line.split(':', 1)
                except ValueError:
                    # Most likely an empty line, but we're being permissive here and ignore failures.
                    # In the end it's all about having the right values available.
                    if line:
                        api.current_logger().warn(
                            'Failed to parse line in manifest: {file}. Line was: `{line}`'.format(file=path,
                                                                                                  line=line),
                            exc_info=True)
                    continue
                data.append(SapHanaManifestEntry(key=key, value=value.strip()))
    except OSError:
        return None
    return data


def search_sap_hana_instances():
    """
    Searches for all instances of SAP HANA on the system and gets the information for it

    This code will go through all entries in /hana/shared and checks for all instances within.
    For each instance it will check the status and record the location.

    :return: SapHanaInfo
    """
    # Keeps track of found instances
    result = []
    # Keeps track if any instance is running
    any_running = False
    # Keeps track if any SAP HANA installation was found. In theory it could be also derived from len(result) != 0
    installed = False
    if path.isdir(HANA_BASE_PATH):
        for entry in listdir(HANA_BASE_PATH):

            entry_path = path.join(HANA_BASE_PATH, entry)
            sapcontrol_path = path.join(entry_path, HANA_SAPCONTROL_PATH)
            entry_manifest_path = path.join(entry_path, HANA_MANIFEST_PATH)
            if path.isfile(entry_manifest_path):
                # We found the manifest file in the expected relative path.
                # Now we are going to look for instance directories.
                for instance in listdir(entry_path):
                    instance_number = None
                    if 'HDB' in instance:
                        # We found an instance. Instance directories follow HDB[0-9][0-9] naming pattern,
                        # where the numbers represent the instance number.
                        instance_number = instance[-2:]
                    if not instance_number:
                        # This is not a folder we are interested in
                        continue
                    # Now obviously SAP HANA is installed
                    installed = True
                    # We can derive the admin name from `entry` directory name
                    admin_name = '{}adm'.format(entry.lower())
                    # Retrieving the status of this instance
                    running = get_instance_status(instance_number, sapcontrol_path, admin_name)
                    # Update the variable that tracks all instances if any is running.
                    # This makes the inhibitor code easier later.
                    any_running = any_running or running
                    # Append the found instance to the list
                    result.append(
                        SapHanaInstanceInfo(
                            name=entry,
                            manifest=parse_manifest(entry_manifest_path),
                            path=entry_path,
                            instance_number=instance_number,
                            running=running,
                            admin=admin_name
                        )
                    )
    # Return the results
    return SapHanaInfo(instances=result, running=any_running, installed=installed)


def get_instance_status(instance_number, sapcontrol_path, admin_name):
    """ Gets the status for the instance given """
    try:
        # Executes sapcontrol in the context of the instance admin user to retrieve the process list for the given
        # SAP HANA instance.
        # GetProcessList has some oddities, like returning non zero exit codes with special meanings.
        # Exit code 3 = All processes are running correctly
        # Exit code 4 = All processes stopped
        # Other exit codes aren't handled at this time and it's assumed that SAP HANA is possibly in some unusual
        # state. Such as starting/stopping but also that it is in some kind of failure state.
        output = run([
            'sudo', '-u', admin_name, sapcontrol_path, '-nr', instance_number, '-function', 'GetProcessList'],
            checked=False)
        if output['exit_code'] == 3:
            # GetProcessList succeeded, all processes running correctly
            return True
        if output['exit_code'] == 4:
            # GetProcessList succeeded, all processes stopped
            return False
        # SAP HANA might be somewhere in between (Starting/Stopping)
        # In that case there are always more than 7 lines.
        return len(output['stdout'].split('\n')) > 7
    except CalledProcessError:
        api.current_logger().warn(
            'Failed to retrieve SAP HANA instance status from sapcontrol - Considering it as not running.')
        return False


# repos/system_upgrade/common/actors/scansaphana/tests/test_scansaphana.py

import subprocess
from io import BytesIO

from leapp.compat import IS_PYTHON3, unicode_type
from leapp.libraries.actor import scansaphana

SAPHANA_TEST_INSTANCES = ('00', '10', '23', '34', '66')
SAPHANA2_MANIFEST = ''.join(('''compversion-id: 73554900100200005327
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
rev-changelist: 1605092543
max_sps12_rev-number: 122
max_sps12_patchlevel: 33
max_rel2.0_sps0_rev-number: 02
max_rel2.0_sps0_patchlevel: 02
max_rel2.0_sps1_rev-number: 12
max_rel2.0_sps1_patchlevel: 05
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
upgrade-restriction: sourceVersion="[1.00.000.00,1.00.69.07)"; message="You must first upgrade your system to the ''',
                             '''latest HANA 1.0 SPS6 Revision (69.07), then to HANA 1.0 SPS12 if you want to ''',
                             '''upgrade to HANA 2.0"
upgrade-restriction: sourceVersion="[1.00.69.07,1.00.120.00)"; message="You must first make an intermediate update ''',
                             '''to HANA SPS12 if you want to upgrade to HANA 2.0. Please see SAP Note 2372809."
restrict-supported-components: name="REMOTE_DATA_SYNC"; vendor="sap.com"; version="[2.0.010.00,3.00.000.00)"
sp-number: 053
sp-patchlevel: 00
makeid: 7694334
date: 2020-11-11 12:12:22
platform: linuxx86_64
hdb-state: RAMP
fullversion: 2.00.053.00 Build 1605092543-1530
auxversion: 0000.00.0
cloud_edition: 0000.00.00
changeinfo: CONT 49fd3e766fbee9a5c22dd609397d2fa640b9df09 (fa/hana2sp05)
compiletype: rel
compilebranch: fa/hana2sp05
git-hash: 49fd3e766fbee9a5c22dd609397d2fa640b9df09
git-headcount: 500015
git-mergetime: 2020-11-11 12:02:23
git-mergeepoch: 1605092543
sapexe-version: 753
sapexe-branch: 753_REL
sapexe-changelist: 2007209
compiler-version-full: gcc (SAP release 20200227, based on SUSE gcc9-9.2.1+r275327-1.3.7) 9.2.1 20190903 ''',
                             '''[gcc-9-branch revision 275330]
compiler-version: GCC 9
lcmserver-artifact-version: 2.5.46'''))


class CallMock(object):
    def __init__(self, ret):
        self.args = None
        self.ret = ret

    def __call__(self, *args, **kwargs):
        self.args = args
        return self.ret


class SubprocessCall(object):
    def __init__(self, admusername):
        self.admusername = admusername

    def __call__(self, *args, **kwargs):
        assert args[0][0:3] == ['sudo', '-u', self.admusername]
        cmd = args[0][3:]
        kwargs.pop('checked', None)
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        p.wait()
        return {'exit_code': p.returncode, 'stdout': p.stdout.read()}


def test_scansaphana_get_instance_status(monkeypatch):
    call = CallMock(ret={})
    monkeypatch.setattr(scansaphana, 'run', call)

    call.ret = {'exit_code': 3, 'stdout': ''}
    assert scansaphana.get_instance_status('00', 'fake/control/path', 'tstadm')
    assert call.args[0] == ['sudo', '-u', 'tstadm', 'fake/control/path', '-nr', '00', '-function', 'GetProcessList']

    call.ret = {'exit_code': 4, 'stdout': ''}
    assert not scansaphana.get_instance_status('00', 'fake/control/path', 'tstadm')
    assert call.args[0] == ['sudo', '-u', 'tstadm', 'fake/control/path', '-nr', '00', '-function', 'GetProcessList']

    call.ret = {'exit_code': 0, 'stdout': '\n'}
    assert not scansaphana.get_instance_status('00', 'fake/control/path', 'tstadm')
    assert call.args[0] == ['sudo', '-u', 'tstadm', 'fake/control/path', '-nr', '00', '-function', 'GetProcessList']

    call.ret = {'exit_code': 0, 'stdout': ' \n' * 6}
    assert not scansaphana.get_instance_status('00', 'fake/control/path', 'tstadm')
    assert call.args[0] == ['sudo', '-u', 'tstadm', 'fake/control/path', '-nr', '00', '-function', 'GetProcessList']

    call.ret = {'exit_code': 0, 'stdout': '\n' * 7}
    assert scansaphana.get_instance_status('00', 'fake/control/path', 'tstadm')
    assert call.args[0] == ['sudo', '-u', 'tstadm', 'fake/control/path', '-nr', '00', '-function', 'GetProcessList']


def test_scansaphana_parse_manifest(monkeypatch):
    class _mock_open(object):
        def __init__(self, path, mode):
            self._fp = BytesIO(SAPHANA2_MANIFEST.encode('utf-8'))

        def __enter__(self):
            return self._fp

        def __exit__(self, *args, **kwargs):
            return None

    monkeypatch.setattr(scansaphana, 'open', _mock_open, False)
    data = scansaphana.parse_manifest('yadda')

    def _get_manifest_entry(key):
        for entry in data:
            if entry.key == key:
                return entry.value
        return None
    assert _get_manifest_entry('sapexe-version') == '753'
    assert _get_manifest_entry('keycaption') == 'SAP HANA DATABASE'


def test_scansaphana_search_saphana_databases(monkeypatch, tmpdir):
    base_path = tmpdir
    monkeypatch.setattr(scansaphana, 'HANA_BASE_PATH', str(base_path))
    cur_path = base_path
    manifest_path_parts = scansaphana.HANA_MANIFEST_PATH.split('/')
    for part in manifest_path_parts[0:-1]:
        cur_path = cur_path.join(part)
        cur_path.mkdir()

    linkpath = base_path.join(manifest_path_parts[0])

    manifest = cur_path.join(manifest_path_parts[-1])
    manifest.write_text(unicode_type(SAPHANA2_MANIFEST), encoding='utf-8')

    sapcontrol = cur_path.join(scansaphana.HANA_SAPCONTROL_PATH.split('/')[-1])
    sapcontrol.write_text(unicode_type('''#!/bin/bash
echo ''
echo '26.01.2021 17:55:17'
echo 'GetProcessList'
echo 'OK'
echo 'name, description, dispstatus, textstatus, starttime, elapsedtime, pid'
echo 'hdbdaemon, HDB Daemon, GREEN, Running, 2020 05 08 21:56:28, 6307:58:49, 16043'
echo 'hdbcompileserver, HDB Compileserver, GREEN, Running, 2020 05 08 21:57:00, 6307:58:17, 16365'
echo 'hdbdiserver, HDB Deployment Infrastructure Server, GREEN, Running, 2020 05 08 21:57:37, 6307:57:40, 16546'
echo 'hdbindexserver, HDB Indexserver-HXE, GREEN, Running, 2020 05 08 21:57:01, 6307:58:16, 16391'
echo 'hdbnameserver, HDB Nameserver, GREEN, Running, 2020 05 08 21:56:29, 6307:58:48, 16061'
echo 'hdbwebdispatcher, HDB Web Dispatcher, GREEN, Running, 2020 05 08 21:57:38, 6307:57:39, 16549'
exit 3
    '''), encoding='utf-8')
    sapcontrol.chmod(0o755)

    result = scansaphana.search_sap_hana_instances()
    assert not result.instances
    assert not result.running
    assert not result.installed

    monkeypatch.setattr(scansaphana, 'run', SubprocessCall('lppadm'))
    admin = base_path.join('LPP')
    admin.mkdir()
    admin.join(manifest_path_parts[0]).mksymlinkto(str(linkpath))

    for instance in SAPHANA_TEST_INSTANCES:
        instance_path = admin.join(('HDB' + instance))
        instance_path.mkdir()

    result = scansaphana.search_sap_hana_instances()
    assert result.instances
    assert len(result.instances) == len(SAPHANA_TEST_INSTANCES)
    check_instance_numbers = set(SAPHANA_TEST_INSTANCES)
    for instance in result.instances:
        assert instance.instance_number in check_instance_numbers
        check_instance_numbers.remove(instance.instance_number)
    assert not check_instance_numbers
    assert result.running
    assert result.installed


# repos/system_upgrade/common/actors/scansubscriptionmanagerinfo/libraries/scanrhsm.py

from leapp.libraries.common import rhsm
from leapp.libraries.common.mounting import NotIsolatedActions
from leapp.libraries.stdlib import api


@rhsm.with_rhsm
def scan():
    context = NotIsolatedActions(base_dir='/')
    info = rhsm.scan_rhsm_info(context)
    api.produce(info)


# repos/system_upgrade/common/actors/scansubscriptionmanagerinfo/tests/test_scansubscriptionmanagementinfo.py

import pytest

from leapp.libraries.actor import scanrhsm
from leapp.libraries.common import rhsm
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import RepositoryData, RepositoryFile, RHSMInfo


def mocked_get_rhsm_info(context):
    assert context, 'The actor did not provide library with valid context.'
    info = RHSMInfo()
    info.attached_skus = ['SKU1', 'SKU2']
    info.available_repos = ['Repo1', 'Repo2']
    info.enabled_repos = ['Repo2']
    info.release = '7.9'
    info.existing_product_certificates = ['Cert1', 'Cert2', 'Cert3']
    info.sca_detected = True
    return info


def test_scansubscriptionmanagementinfo(monkeypatch):
    actor_producs = produce_mocked()

    monkeypatch.setattr(rhsm, 'scan_rhsm_info', mocked_get_rhsm_info)
    monkeypatch.setattr(api, 'produce', actor_producs)
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked())

    scanrhsm.scan()

    assert actor_producs.model_instances, 'The actor did not produce any message.'
    assert len(actor_producs.model_instances) == 1, 'The actor produced more messages than expected.'

    message = actor_producs.model_instances[0]

    # The actor does not do much more than calling the `rhsm` library (which has its own tests),
    # just check that the message has not changed
    assert message.attached_skus == ['SKU1', 'SKU2']
    assert message.available_repos == ['Repo1', 'Repo2']
    assert message.enabled_repos == ['Repo2']
    assert message.release == '7.9'
    assert message.existing_product_certificates == ['Cert1', 'Cert2', 'Cert3']
    assert message.sca_detected


# repos/system_upgrade/common/actors/scantargetiso/libraries/scan_target_os_iso.py

import os

import leapp.libraries.common.config as ipu_config
from leapp.libraries.common.mounting import LoopMount, MountError
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import CustomTargetRepository, TargetOSInstallationImage


def determine_rhel_version_from_iso_mountpoint(iso_mountpoint):
    baseos_packages = os.path.join(iso_mountpoint, 'BaseOS/Packages')
    if os.path.isdir(baseos_packages):
        def is_rh_release_pkg(pkg_name):
            return pkg_name.startswith('redhat-release') and 'eula' not in pkg_name

        redhat_release_pkgs = [pkg for pkg in os.listdir(baseos_packages) if is_rh_release_pkg(pkg)]

        if not redhat_release_pkgs:
            return ''  # We did not determine anything

        if len(redhat_release_pkgs) > 1:
            api.current_logger().warn('Multiple packages with name redhat-release* found when '
                                      'determining RHEL version of the supplied installation ISO.')

        redhat_release_pkg = redhat_release_pkgs[0]

        determined_rhel_ver = ''
        try:
            rh_release_pkg_path = os.path.join(baseos_packages, redhat_release_pkg)
            # rpm2cpio is provided by rpm; cpio is a dependency of yum (rhel7) and a dependency of dracut which is
            # a dependency for leapp (rhel8+)
            cpio_archive = run(['rpm2cpio', rh_release_pkg_path])
            etc_rh_release_contents = run(['cpio', '--extract', '--to-stdout', './etc/redhat-release'],
                                          stdin=cpio_archive['stdout'])

            # 'Red Hat Enterprise Linux Server release 7.9 (Maipo)' -> ['Red Hat...', '7.9 (Maipo']
            product_release_fragments = etc_rh_release_contents['stdout'].split('release')
            if len(product_release_fragments) != 2:
                return ''  # Unlikely. Either way we failed to parse the release

            if not product_release_fragments[0].startswith('Red Hat'):
                return ''

            determined_rhel_ver = product_release_fragments[1].strip().split(' ', 1)[0]  # Remove release name (Maipo)
            return determined_rhel_ver
        except CalledProcessError:
            return ''
    return ''


def inform_ipu_about_request_to_use_target_iso():
    target_iso_path = ipu_config.get_env('LEAPP_TARGET_ISO')
    if not target_iso_path:
        return

    iso_mountpoint = '/iso'

    if not os.path.exists(target_iso_path):
        # If the path does not exists, do not attempt to mount it and let the upgrade be inhibited by the check actor
        api.produce(TargetOSInstallationImage(path=target_iso_path,
                                              repositories=[],
                                              mountpoint=iso_mountpoint,
                                              was_mounted_successfully=False))
        return

    # Mount the given ISO, extract the available repositories and determine provided RHEL version
    iso_scan_mountpoint = '/var/lib/leapp/iso_scan_mountpoint'
    try:
        with LoopMount(source=target_iso_path, target=iso_scan_mountpoint):
            required_repositories = ('BaseOS', 'AppStream')

            # Check what required repositories are present in the root of the ISO
            iso_contents = os.listdir(iso_scan_mountpoint)
            present_repositories = [req_repo for req_repo in required_repositories if req_repo in iso_contents]

            # Create custom repository information about the repositories found in the root of the ISO
            iso_repos = []
            for repo_dir in present_repositories:
                baseurl = 'file://' + os.path.join(iso_mountpoint, repo_dir)
                iso_repo = CustomTargetRepository(name=repo_dir, baseurl=baseurl, repoid=repo_dir)
                api.produce(iso_repo)
                iso_repos.append(iso_repo)

            rhel_version = determine_rhel_version_from_iso_mountpoint(iso_scan_mountpoint)

            api.produce(TargetOSInstallationImage(path=target_iso_path,
                                                  repositories=iso_repos,
                                                  mountpoint=iso_mountpoint,
                                                  rhel_version=rhel_version,
                                                  was_mounted_successfully=True))
    except MountError:
        # Do not analyze the situation any further as ISO checks will be done by another actor
        iso_mountpoint = '/iso'
        api.produce(TargetOSInstallationImage(path=target_iso_path,
                                              repositories=[],
                                              mountpoint=iso_mountpoint,
                                              was_mounted_successfully=False))


# repos/system_upgrade/common/actors/scantargetiso/tests/test_scan_target_iso.py

import contextlib
import os
from functools import partial

import pytest

from leapp.libraries.actor import scan_target_os_iso
from leapp.libraries.common.mounting import MountError
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import CustomTargetRepository, TargetOSInstallationImage


def fail_if_called(fail_reason, *args, **kwargs):
    assert False, fail_reason


def test_determine_rhel_version_determination_unexpected_iso_structure_or_invalid_mountpoint(monkeypatch):
    iso_mountpoint = '/some/mountpoint'

    run_mocked = partial(fail_if_called,
                         'No commands should be called when mounted ISO mountpoint has unexpected structure.')
    monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)

    def isdir_mocked(path):
        assert path == '/some/mountpoint/BaseOS/Packages', 'Only the contents of BaseOS/Packages should be examined.'
        return False

    monkeypatch.setattr(os.path, 'isdir', isdir_mocked)

    determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
    assert not determined_version


def test_determine_rhel_version_valid_iso(monkeypatch):
    iso_mountpoint = '/some/mountpoint'

    def isdir_mocked(path):
        return True

    def listdir_mocked(path):
        assert path == '/some/mountpoint/BaseOS/Packages', 'Only the contents of BaseOS/Packages should be examined.'
        return ['xz-5.2.4-4.el8_6.x86_64.rpm',
                'libmodman-2.0.1-17.el8.i686.rpm',
                'redhat-release-8.7-0.3.el8.x86_64.rpm',
                'redhat-release-eula-8.7-0.3.el8.x86_64.rpm']

    def run_mocked(cmd, *args, **kwargs):
        rpm2cpio_output = 'rpm2cpio_output'
        if cmd[0] == 'rpm2cpio':
            assert cmd == ['rpm2cpio', '/some/mountpoint/BaseOS/Packages/redhat-release-8.7-0.3.el8.x86_64.rpm']
            return {'stdout': rpm2cpio_output}
        if cmd[0] == 'cpio':
            assert cmd == ['cpio', '--extract', '--to-stdout', './etc/redhat-release']
            assert kwargs['stdin'] == rpm2cpio_output
            return {'stdout': 'Red Hat Enterprise Linux Server release 7.9 (Maipo)'}
        raise ValueError('Unexpected command has been called.')

    monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
    monkeypatch.setattr(os, 'listdir', listdir_mocked)
    monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)

    determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
    assert determined_version == '7.9'


def test_determine_rhel_version_valid_iso_no_rh_release(monkeypatch):
    iso_mountpoint = '/some/mountpoint'

    def isdir_mocked(path):
        return True

    def listdir_mocked(path):
        assert path == '/some/mountpoint/BaseOS/Packages', 'Only the contents of BaseOS/Packages should be examined.'
        return ['xz-5.2.4-4.el8_6.x86_64.rpm',
                'libmodman-2.0.1-17.el8.i686.rpm',
                'redhat-release-eula-8.7-0.3.el8.x86_64.rpm']

    run_mocked = partial(fail_if_called, 'No command should be called if the redhat-release package is not present.')

    monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
    monkeypatch.setattr(os, 'listdir', listdir_mocked)
    monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)

    determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
    assert determined_version == ''


def test_determine_rhel_version_rpm_extract_fails(monkeypatch):
    iso_mountpoint = '/some/mountpoint'

    def isdir_mocked(path):
        return True

    def listdir_mocked(path):
        assert path == '/some/mountpoint/BaseOS/Packages', 'Only the contents of BaseOS/Packages should be examined.'
        return ['redhat-release-8.7-0.3.el8.x86_64.rpm']

    def run_mocked(cmd, *args, **kwargs):
        raise CalledProcessError(message='Ooops.', command=cmd, result=2)

    monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
    monkeypatch.setattr(os, 'listdir', listdir_mocked)
    monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)

    determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
    assert determined_version == ''


@pytest.mark.parametrize('etc_rh_release_contents', ('',
                                                     'Red Hat Enterprise Linux Server',
                                                     'Fedora release 35 (Thirty Five)'))
def test_determine_rhel_version_unexpected_etc_rh_release_contents(monkeypatch, etc_rh_release_contents):
    iso_mountpoint = '/some/mountpoint'

    def isdir_mocked(path):
        return True

    def listdir_mocked(path):
        assert path == '/some/mountpoint/BaseOS/Packages', 'Only the contents of BaseOS/Packages should be examined.'
        return ['redhat-release-8.7-0.3.el8.x86_64.rpm']

    def run_mocked(cmd, *args, **kwargs):
        if cmd[0] == 'rpm2cpio':
            return {'stdout': 'rpm2cpio_output'}
        if cmd[0] == 'cpio':
            return {'stdout': etc_rh_release_contents}
        raise ValueError('Actor called an unexpected command: {0}'.format(cmd))

    monkeypatch.setattr(os.path, 'isdir', isdir_mocked)
    monkeypatch.setattr(os, 'listdir', listdir_mocked)
    monkeypatch.setattr(scan_target_os_iso, 'run', run_mocked)

    determined_version = scan_target_os_iso.determine_rhel_version_from_iso_mountpoint(iso_mountpoint)
    assert determined_version == ''


@pytest.mark.parametrize('iso_envar_set', (True, False))
def test_iso_detection_with_no_iso(monkeypatch, iso_envar_set):
    envars = {'LEAPP_TARGET_ISO': '/target_iso'} if iso_envar_set else {}
    mocked_actor = CurrentActorMocked(envars=envars)
    monkeypatch.setattr(api, 'current_actor', mocked_actor)
    monkeypatch.setattr(api, 'produce', produce_mocked())

    scan_target_os_iso.inform_ipu_about_request_to_use_target_iso()
    assert bool(api.produce.called) == iso_envar_set


def test_iso_mounting_failed(monkeypatch):
    envars = {'LEAPP_TARGET_ISO': '/target_iso'}
    mocked_actor = CurrentActorMocked(envars=envars)
    monkeypatch.setattr(api, 'current_actor', mocked_actor)
    monkeypatch.setattr(api, 'produce', produce_mocked())

    def raise_mount_error_when_called():
        raise MountError('MountError')

    monkeypatch.setattr(scan_target_os_iso, 'LoopMount', raise_mount_error_when_called)

    scan_target_os_iso.inform_ipu_about_request_to_use_target_iso()
    assert api.produce.called

    assert len(api.produce.model_instances) == 1
    assert not api.produce.model_instances[0].was_mounted_successfully


@pytest.mark.parametrize(('repodirs_in_iso', 'expected_repoids'),
                         (((), ()),
                          (('BaseOS',), ('BaseOS',)),
                          (('BaseOS', 'AppStream'), ('BaseOS', 'AppStream')),
                          (('BaseOS', 'AppStream', 'UnknownRepo'), ('BaseOS', 'AppStream'))))
def test_iso_repository_detection(monkeypatch, repodirs_in_iso, expected_repoids):
    iso_path = '/target_iso'
    envars = {'LEAPP_TARGET_ISO': iso_path}
    mocked_actor = CurrentActorMocked(envars=envars)

    @contextlib.contextmanager
    def always_successful_loop_mount(*args, **kwargs):
        yield

    def mocked_os_path_exits(path):
        if path == iso_path:
            return True
        raise ValueError('Only the ISO path should be probed for existence.')

    def mocked_os_listdir(path):
        # Add some extra files as an ISO will always have some extra files in / as the ones parametrizing this test
        return list(repodirs_in_iso + ('eula.txt', 'grub', 'imgs'))

    monkeypatch.setattr(api, 'current_actor', mocked_actor)
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(scan_target_os_iso, 'LoopMount', always_successful_loop_mount)
    monkeypatch.setattr(os.path, 'exists', mocked_os_path_exits)
    monkeypatch.setattr(os, 'listdir', mocked_os_listdir)
    monkeypatch.setattr(scan_target_os_iso, 'determine_rhel_version_from_iso_mountpoint', lambda iso_mountpoint: '7.9')

    scan_target_os_iso.inform_ipu_about_request_to_use_target_iso()

    produced_msgs = api.produce.model_instances
    assert len(produced_msgs) == 1 + len(expected_repoids)

    produced_custom_repo_msgs = []
    target_iso_msg = None
    for produced_msg in produced_msgs:
        if isinstance(produced_msg, CustomTargetRepository):
            produced_custom_repo_msgs.append(produced_msg)
        else:
            assert not target_iso_msg, 'Actor is expected to produce only one TargetOSInstallationImage msg'
            target_iso = produced_msg

    # Do not explicitly instantiate model instances of what we expect the model instance to look like. Instead check
    # for expected structural properties, leaving the actor implementation flexibility (e.g. choice of the mountpoint)
    iso_mountpoint = target_iso.mountpoint

    assert target_iso.was_mounted_successfully
    assert target_iso.rhel_version == '7.9'

    expected_repos = {(repoid, 'file://' + os.path.join(iso_mountpoint, repoid)) for repoid in expected_repoids}
    actual_repos = {(repo.repoid, repo.baseurl) for repo in produced_custom_repo_msgs}
    assert expected_repos == actual_repos


# repos/system_upgrade/common/actors/scanzfcp/libraries/scanzfcp.py

import os

from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api
from leapp.models import CopyFile, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks

ZFCP_CONF = '/etc/zfcp.conf'


def process():
    if not architecture.matches_architecture(architecture.ARCH_S390X):
        return
    copy_files = []
    if os.path.isfile(ZFCP_CONF):
        # the file has to be copied into the targetuserspace container first,
        # then it can be included into the initramfs ==> both messages are
        # needed to be produced
        copy_files = [CopyFile(src=ZFCP_CONF)]
        api.produce(UpgradeInitramfsTasks(include_files=[ZFCP_CONF]))
    else:
        api.current_logger().info(
            "The {} file has not been discovered. ZFCP not used."
            .format(ZFCP_CONF)
        )
    api.produce(TargetUserSpaceUpgradeTasks(copy_files=copy_files, install_rpms=['s390utils-core']))


# repos/system_upgrade/common/actors/scanzfcp/tests/unit_test_scanzfcp.py

import os

import pytest

from leapp.libraries.actor import scanzfcp
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
from leapp.models import CopyFile, TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks


def test_zfcp_exists(monkeypatch):
    monkeypatch.setattr(scanzfcp.api, 'current_actor', CurrentActorMocked(arch=architecture.ARCH_S390X))
    monkeypatch.setattr(scanzfcp.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(scanzfcp.api, 'produce', produce_mocked())
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: True)
    scanzfcp.process()
    assert not scanzfcp.api.current_logger.infomsg
    assert scanzfcp.api.produce.called == 2
    tusut_flag = False
    uit_flag = False
    for msg in scanzfcp.api.produce.model_instances:
        if isinstance(msg, TargetUserSpaceUpgradeTasks):
            assert [CopyFile(src=scanzfcp.ZFCP_CONF)] == msg.copy_files
            assert msg.install_rpms == ['s390utils-core']
            tusut_flag = True
        elif isinstance(msg, UpgradeInitramfsTasks):
            assert [scanzfcp.ZFCP_CONF] == msg.include_files
            uit_flag = True
    assert tusut_flag and uit_flag


def test_zfcp_not_found(monkeypatch):
    monkeypatch.setattr(scanzfcp.api, 'current_actor', CurrentActorMocked(arch=architecture.ARCH_S390X))
    monkeypatch.setattr(scanzfcp.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(scanzfcp.os.path, 'isfile', lambda dummy: False)
    monkeypatch.setattr(scanzfcp.api, 'produce', produce_mocked())
    scanzfcp.process()
    assert scanzfcp.api.current_logger.infomsg
    assert scanzfcp.api.produce.called == 1
    assert len(scanzfcp.api.produce.model_instances) == 1
    assert isinstance(scanzfcp.api.produce.model_instances[0], TargetUserSpaceUpgradeTasks)
    assert scanzfcp.api.produce.model_instances[0].install_rpms == ['s390utils-core']
    assert not scanzfcp.api.produce.model_instances[0].copy_files


@pytest.mark.parametrize('isfile', [True, False])
@pytest.mark.parametrize('arch', [
    architecture.ARCH_X86_64,
    architecture.ARCH_ARM64,
    architecture.ARCH_PPC64LE,
])
def test_non_ibmz_arch(monkeypatch, isfile, arch):
    monkeypatch.setattr(scanzfcp.api, 'current_actor', CurrentActorMocked(arch=arch))
    monkeypatch.setattr(scanzfcp.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(scanzfcp.api, 'produce', produce_mocked())
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: isfile)
    scanzfcp.process()
    assert not scanzfcp.api.current_logger.infomsg
    assert not scanzfcp.api.produce.called


# repos/system_upgrade/common/actors/scheduleselinuxrelabeling/tests/test_scheduleselinuxrelabeling.py

import os

import pytest

from leapp.models import SelinuxRelabelDecision
from leapp.snactor.fixture import current_actor_context

# TODO These tests modifies the system


@pytest.mark.skipif(
    os.getenv("DESTRUCTIVE_TESTING", False) in [False, "0"],
    reason='Test disabled by default because it would modify the system',
)
def test_schedule_no_relabel(current_actor_context):
    current_actor_context.feed(SelinuxRelabelDecision(set_relabel=False))
    current_actor_context.run()
    assert not os.path.isfile('/.autorelabel')


@pytest.mark.skipif(
    os.getenv("DESTRUCTIVE_TESTING", False) in [False, "0"],
    reason='Test disabled by default because it would modify the system',
)
def test_schedule_relabel(current_actor_context):
    current_actor_context.feed(SelinuxRelabelDecision(set_relabel=True))
    current_actor_context.run()
    assert os.path.isfile('/.autorelabel')

    # lets cleanup so we possibly not affect further testing
    os.unlink('/.autorelabel')


# repos/system_upgrade/common/actors/selinux/selinuxapplycustom/actor.py

import os
import shutil

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.actor import selinuxapplycustom
from leapp.libraries.actor.selinuxapplycustom import BACKUP_DIRECTORY
from leapp.libraries.stdlib import CalledProcessError, run
from leapp.models import SELinuxCustom, SELinuxModules
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag

WORKING_DIRECTORY = '/tmp/selinux/'


class SELinuxApplyCustom(Actor):
    """
    Re-apply SELinux customizations from the original RHEL installation

    Re-apply SELinux policy customizations (custom policy modules and changes
    introduced by semanage). Any changes (due to incompatibility with
    SELinux policy in the upgraded system) are reported to user.
    """
    name = 'selinuxapplycustom'
    consumes = (SELinuxCustom, SELinuxModules)
    produces = (reporting.Report,)
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        # save progress for repoting purposes
        failed_modules = []
        failed_custom = []

        # clear working directory
        shutil.rmtree(WORKING_DIRECTORY, ignore_errors=True)

        try:
            os.mkdir(WORKING_DIRECTORY)
        except OSError:
            self.log.warning('Failed to create working directory! Aborting.')
            return

        # get list of policy modules after the upgrade
        installed_modules = set(
            [module[0] for module in selinuxapplycustom.list_selinux_modules()]
        )

        # import custom SElinux modules
        for semodules in self.consume(SELinuxModules):
            self.log.info(
                'Processing custom SELinux policy modules. Count: {}.'.format(len(semodules.modules))
            )
            # check for presence of udica templates and make sure to install their latest versions
            selinuxapplycustom.install_udica_templates(semodules.templates)

            if not semodules.modules:
                continue

            command = ['semodule']
            for module in semodules.modules:
                # Skip modules that are already installed. This prevents DSP modules installed with wrong
                # priority (usually 400) from being overwritten by an older version
                if module.name in installed_modules:
                    self.log.info(
                        'Skipping module {} on priority {} because it is already installed.'.format(
                            module.name,
                            module.priority
                        )
                    )
                    continue

                # cil module files need to be extracted to disk in order to be installed
                cil_filename = os.path.join(
                    WORKING_DIRECTORY, '{}.cil'.format(module.name)
                )
                self.log.info(
                    'Installing module {} on priority {}.'.format(module.name, module.priority)
                )
                if module.removed:
                    self.log.warning(
                        '{}: The following lines where removed because of incompatibility:\n{}'.format(
                            module.name,
                            '\n'.join(module.removed)
                        )
                    )
                # write module content to disk
                try:
                    with open(cil_filename, 'w') as cil_file:
                        cil_file.write(module.content)
                except OSError as e:
                    self.log.warning('Error writing {} : {}'.format(cil_filename, e))
                    continue

                command.extend(['-X', str(module.priority), '-i', cil_filename])

            try:
                run(command)
            except CalledProcessError as e:
                self.log.warning(
                    'Error installing modules in a single transaction:'
                    '{}\nRetrying -- now each module will be installed separately.'.format(e.stderr)
                )
                # Retry, but install each module separately
                for module in semodules.modules:
                    if module.name in installed_modules:
                        continue
                    cil_filename = os.path.join(
                        WORKING_DIRECTORY, '{}.cil'.format(module.name)
                    )
                    self.log.info(
                        'Installing module {} on priority {}.'.format(module.name, module.priority)
                    )
                    try:
                        run(['semodule',
                             '-X', str(module.priority),
                             '-i', cil_filename
                             ]
                            )
                    except CalledProcessError as e:
                        self.log.warning('Error installing module: {}'.format(e.stderr))
                        failed_modules.append(module.name)
                        selinuxapplycustom.back_up_failed(cil_filename)
                        continue

        # import SELinux customizations collected by "semanage export"
        for custom in self.consume(SELinuxCustom):
            self.log.info(
                'Importing the following SELinux customizations collected by "semanage export":\n{}'.format(
                    '\n'.join(custom.commands)
                )
            )
            # import customizations
            try:
                run(['semanage', 'import'], stdin='\n'.join(custom.commands))
            except CalledProcessError as e:
                self.log.warning(
                    'Error importing SELinux customizations in a single transaction:'
                    '{}\nRetrying -- now each command will be applied separately.'.format(e.stderr)
                )
                for cmd in custom.commands:
                    try:
                        run(['semanage', 'import'], stdin='{}\n'.format(cmd))
                    except CalledProcessError as e:
                        self.log.warning('Error applying "semanage {}": {}'.format(cmd, e.stderr))
                        failed_custom.append(cmd)
                continue

        # clean-up
        shutil.rmtree(WORKING_DIRECTORY, ignore_errors=True)

        if failed_modules or failed_custom:
            summary = ''
            if failed_modules:
                summary = (
                    'The following policy modules couldn\'t be installed: {}.\n'
                    'You can review their content in {}.'.format(
                        ', '.join(failed_modules), BACKUP_DIRECTORY
                    )
                )
            if failed_custom:
                if summary:
                    summary = '{}\n\n'.format(summary)
                summary = '{}The following commands couldn\'t be applied:\n{}'.format(
                    summary, '\n'.join(['semanage {}'.format(x) for x in failed_custom])
                )

            reporting.create_report(
                [
                    reporting.Title(
                        'SELinux failed to reapply some customizations after the upgrade.'
                    ),
                    reporting.Summary(summary),
                    reporting.Severity(reporting.Severity.MEDIUM),
                    reporting.Groups([reporting.Groups.SECURITY, reporting.Groups.SELINUX]),
                ]
                + [
                    reporting.RelatedResource(
                        'file', os.path.join(BACKUP_DIRECTORY, '{}.cil'.format(x))
                    )
                    for x in failed_modules
                ]
            )


# repos/system_upgrade/common/actors/selinux/selinuxcontentscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import selinuxcontentscanner
from leapp.models import RpmTransactionTasks, SELinuxCustom, SELinuxFacts, SELinuxModules, SELinuxRequestRPMs
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class SELinuxContentScanner(Actor):
    """
    Scan the system for any SELinux customizations

    Find SELinux policy customizations (custom policy modules and changes
    introduced by semanage) and save them in SELinuxModules and SELinuxCustom
    models. Customizations that are incompatible with SELinux policy on RHEL-8
    are removed.
    """

    name = 'selinuxcontentscanner'
    consumes = (SELinuxFacts,)
    produces = (SELinuxModules, SELinuxCustom, SELinuxRequestRPMs, RpmTransactionTasks)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        # exit if SELinux is disabled
        for fact in self.consume(SELinuxFacts):
            if not fact.enabled:
                return

        (semodule_list, template_list, rpms_to_install,) = selinuxcontentscanner.get_selinux_modules()

        self.produce(
            SELinuxModules(
                modules=semodule_list,
                templates=template_list
            )
        )
        self.produce(
            RpmTransactionTasks(
                to_install=rpms_to_install
            )
        )
        # this is produced so that we can later verify that the RPMs are present after upgrade
        self.produce(
            SELinuxRequestRPMs(
                to_install=rpms_to_install
            )
        )

        (semanage_valid, semanage_removed,) = selinuxcontentscanner.get_selinux_customizations()

        self.produce(
            SELinuxCustom(
                commands=semanage_valid,
                removed=semanage_removed
            )
        )


# repos/system_upgrade/common/actors/selinux/selinuxprepare/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import selinuxprepare
from leapp.models import SELinuxCustom, SELinuxModules
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class SELinuxPrepare(Actor):
    """
    Remove selinux policy customizations before updating selinux-policy* packages

    Policy customizations in the original system could cause policy package
    upgrade to fail and therefore need to be removed.
    Customizations introduced by semanage are removed first, followed by custom
    policy modules gathered by SELinuxContentScanner.
    """

    name = 'selinuxprepare'
    consumes = (SELinuxCustom, SELinuxModules)
    produces = ()
    tags = (PreparationPhaseTag, IPUWorkflowTag)

    def process(self):
        selinuxprepare.remove_semanage_customizations()
        selinuxprepare.remove_custom_modules()


# repos/system_upgrade/common/actors/setetcreleasever/libraries/setetcreleasever.py

from leapp.libraries.stdlib import api
from leapp.models import PkgManagerInfo, RHUIInfo


def _set_releasever(releasever):
    releasever_path = '/etc/dnf/vars/releasever'

    with open(releasever_path, 'w') as fo:
        fo.write(releasever+'\n')


def process():
    target_version = api.current_actor().configuration.version.target

    pkg_facts = next(api.consume(PkgManagerInfo), None)
    rhui_facts = next(api.consume(RHUIInfo), None)

    if pkg_facts and pkg_facts.etc_releasever is not None or rhui_facts:
        # if "/etc/dnf/vars/releasever" file exists, or we are using RHUI, let's set it to our
        # target version.
        _set_releasever(target_version)
    else:
        api.current_logger().debug(
            'Skipping execution. "releasever" is not set in DNF/YUM vars directory and no RHUIInfo has '
            'been produced'
        )


# repos/system_upgrade/common/actors/setetcreleasever/tests/test_setetcreleasever.py

import os

import pytest

from leapp.libraries.actor import setetcreleasever
from leapp.libraries.common.testutils import (
    create_report_mocked,
    CurrentActorMocked,
    logger_mocked
)
from leapp.libraries.stdlib import api
from leapp.models import PkgManagerInfo, RHUIInfo

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


class mocked_set_releasever(object):
    def __init__(self):
        self.content = None

    def __call__(self, content):
        self.content = content


# def test_set_releasever(monkeypatch):

#     target = '8.0'
#     expected_rel_ver = '8.0'
#     monkeypatch.setattr(setetcreleasever, '_set_releasever', mocked_set_releasever())

#     setetcreleasever.process(target)
#     assert expected_rel_ver == setetcreleasever._set_releasever.content


def test_set_releasever(monkeypatch, current_actor_context):

    msgs = [RHUIInfo(provider='aws'), PkgManagerInfo(etc_releasever='7.7')]

    expected_rel_ver = '8.0'
    monkeypatch.setattr(setetcreleasever, '_set_releasever', mocked_set_releasever())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=msgs, dst_ver=expected_rel_ver
        )
    )
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    setetcreleasever.process()

    assert expected_rel_ver == setetcreleasever._set_releasever.content
    assert not api.current_logger.dbgmsg


def test_no_set_releasever(monkeypatch, current_actor_context):

    expected_rel_ver = '8.0'
    monkeypatch.setattr(setetcreleasever, '_set_releasever', mocked_set_releasever())
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(dst_ver=expected_rel_ver))
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    setetcreleasever.process()

    assert not setetcreleasever._set_releasever.content
    assert api.current_logger.dbgmsg


# repos/system_upgrade/common/actors/setpermissiveselinux/libraries/setpermissiveselinux.py

from leapp.libraries.stdlib import CalledProcessError, run


def selinux_set_permissive():
    """ Set SElinux to permissive mode if it was in enforcing mode """
    cmd = ['/bin/sed', '-i', 's/^SELINUX=enforcing/SELINUX=permissive/g', '/etc/selinux/config']
    try:
        run(cmd)
    except CalledProcessError as e:
        return False, e.output
    else:
        return True, None


# repos/system_upgrade/common/actors/setpermissiveselinux/tests/test_setpermissiveselinux.py

import os

import pytest

from leapp.models import SelinuxPermissiveDecision


@pytest.mark.skipif(
    os.getenv("DESTRUCTIVE_TESTING", False) in [False, "0"],
    reason='Test disabled by default because it would modify the system')
def check_permissive_in_conf():
    """ Check if we have set permissive in SElinux conf file """
    with open('/etc/selinux/config') as fo:
        result = [l for l in (line.strip() for line in fo) if l]
        for res in result:
            if res == "SELINUX=permissive":
                return True
    return False


@pytest.mark.skipif(
    os.getenv("DESTRUCTIVE_TESTING", False) in [False, "0"],
    reason='Test disabled by default because it would modify the system')
def test_set_selinux_permissive(current_actor_context):
    current_actor_context.feed(SelinuxPermissiveDecision(set_permissive=True))
    current_actor_context.run()
    assert check_permissive_in_conf()


# repos/system_upgrade/common/actors/setuptargetrepos/libraries/setuptargetrepos.py


from leapp.libraries.actor import setuptargetrepos_repomap
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import api
from leapp.models import (
    CustomTargetRepository,
    RepositoriesBlacklisted,
    RepositoriesFacts,
    RepositoriesMapping,
    RepositoriesSetupTasks,
    RHELTargetRepository,
    RHUIInfo,
    SkippedRepositories,
    TargetRepositories,
    UsedRepositories
)


def _get_enabled_repoids():
    """
    Collects repoids of all enabled repositories on the source system.

    :param repositories_facts: Iterable of RepositoriesFacts containing info about repositories on the source system.
    :returns: Set of all enabled repository IDs present on the source system.
    :rtype: Set[str]
    """
    enabled_repoids = set()
    for repos in api.consume(RepositoriesFacts):
        for repo_file in repos.repositories:
            for repo in repo_file.data:
                if repo.enabled:
                    enabled_repoids.add(repo.repoid)
    return enabled_repoids


def _get_blacklisted_repoids():
    repos_blacklisted = set()
    for blacklist in api.consume(RepositoriesBlacklisted):
        repos_blacklisted.update(blacklist.repoids)
    return repos_blacklisted


def _get_custom_target_repos():
    custom_repos = []
    for repo in api.consume(CustomTargetRepository):
        custom_repos.append(repo)
    return custom_repos


def _get_used_repo_dict():
    """
    Return dict: {used_repoid: [installed_packages]}
    """
    used = {}
    for used_repos in api.consume(UsedRepositories):
        for used_repo in used_repos.repositories:
            used[used_repo.repository] = used_repo.packages
    return used


def _setup_repomap_handler(src_repoids):
    repo_mappig_msg = next(api.consume(RepositoriesMapping), RepositoriesMapping())
    rhui_info = next(api.consume(RHUIInfo), RHUIInfo(provider=''))
    repomap = setuptargetrepos_repomap.RepoMapDataHandler(repo_mappig_msg, cloud_provider=rhui_info.provider)
    # TODO(pstodulk): what about skip this completely and keep the default 'ga'..?
    default_channels = setuptargetrepos_repomap.get_default_repository_channels(repomap, src_repoids)
    repomap.set_default_channels(default_channels)
    return repomap


def _get_mapped_repoids(repomap, src_repoids):
    mapped_repoids = set()
    src_maj_ver = get_source_major_version()
    for repoid in src_repoids:
        if repomap.get_pesid_repo_entry(repoid, src_maj_ver):
            mapped_repoids.add(repoid)
    return mapped_repoids


def process():
    # load all data / messages
    used_repoids_dict = _get_used_repo_dict()
    enabled_repoids = _get_enabled_repoids()
    excluded_repoids = _get_blacklisted_repoids()
    custom_repos = _get_custom_target_repos()

    # TODO(pstodulk): isn't that a potential issue that we map just enabled repos
    # instead of enabled + used repos??
    # initialise basic data
    repomap = _setup_repomap_handler(enabled_repoids)
    mapped_repoids = _get_mapped_repoids(repomap, enabled_repoids)
    skipped_repoids = enabled_repoids & set(used_repoids_dict.keys()) - mapped_repoids

    # Now get the info what should be the target RHEL repositories
    expected_repos = repomap.get_expected_target_pesid_repos(enabled_repoids)
    target_rhel_repoids = set()
    for target_pesid, target_pesidrepo in expected_repos.items():
        if not target_pesidrepo:
            # With the original repomap data, this should not happen (this should
            # currently point to a problem in our data
            # TODO(pstodulk): add report? inhibitor? what should be in the report?
            api.current_logger().error(
                'Missing target repository from the {} family (PES ID).'
                .format(target_pesid)
            )
            continue
        if target_pesidrepo.repoid in excluded_repoids:
            api.current_logger().debug('Skipping the {} repo (excluded).'.format(target_pesidrepo.repoid))
            continue
        target_rhel_repoids.add(target_pesidrepo.repoid)

    # FIXME: this could possibly result into a try to enable multiple repositories
    # from the same family (pesid). But unless we have a bug in previous actors,
    # it should not happen :) it's not blocker error anyway, so survive it.
    # - We expect to deliver the fix as part of the refactoring when we merge
    # setuptargetrepos & peseventsscanner actors together (+ blacklistrepos?)
    for task in api.consume(RepositoriesSetupTasks):
        for repo in task.to_enable:
            if repo in excluded_repoids:
                api.current_logger().debug('Skipping the {} repo from setup task (excluded).'.format(repo))
                continue
            target_rhel_repoids.add(repo)

    # create the final lists and sort them (for easier testing)
    rhel_repos = [RHELTargetRepository(repoid=repoid) for repoid in sorted(target_rhel_repoids)]
    custom_repos = [repo for repo in custom_repos if repo.repoid not in excluded_repoids]
    custom_repos = sorted(custom_repos, key=lambda x: x.repoid)

    if skipped_repoids:
        pkgs = set()
        for repo in skipped_repoids:
            pkgs.update(used_repoids_dict[repo])
        api.produce(SkippedRepositories(repos=sorted(skipped_repoids), packages=sorted(pkgs)))

    api.produce(TargetRepositories(
        rhel_repos=rhel_repos,
        custom_repos=custom_repos,
    ))


# repos/system_upgrade/common/actors/setuptargetrepos/libraries/setuptargetrepos_repomap.py

from leapp.libraries.common.config import get_target_product_channel
from leapp.libraries.common.config.version import get_source_major_version, get_target_major_version
from leapp.libraries.stdlib import api

DEFAULT_PESID = {
    '7': 'rhel7-base',
    '8': 'rhel8-BaseOS',
    '9': 'rhel9-BaseOS'
}


def _get_channel_prio(pesid_repo):
    priorities = {
        'beta': 0,
        'ga': 1,
    }
    return priorities.get(pesid_repo.channel, 10)


class RepoMapDataHandler(object):
    """
    Provide the basic functionality to work with the repository data easily.
    """

    def __init__(self, repo_map, cloud_provider='', default_channels=None):
        """
        Initialize the object based on the given RepositoriesMapping msg.

        Expects that msg contains just stuff related for the current IPU
        (at least mapping and repos for the used upgrade path and architecture).

        :param repo_map: A valid RepositoryMapping message.
        :type repo_map: RepositoryMapping
        :param default_channels: A list of default channels to use when a target repository
                                 equivalent exactly matching a source repository was not found.
        :type default_channels: List[str]
        :param prio_channel: Prefer repositories with this channel when looking for target equivalents.
        :type prio_channel: str
        """
        # NOTE: currently I am keeping this default data structure that is not
        # ideal for work, but there is not any significant impact..
        self.repositories = repo_map.repositories
        self.mapping = repo_map.mapping
        # FIXME(pstodulk): what about default_channel -> fallback_channel
        # hardcoded always as ga? instead of list of channels..
        # it'd be possibly confusing naming now...
        self.default_channels = default_channels or ['ga']

        # Make self.prio_channel None if the user did not specify any target channels, so that self.default_channels
        # will be used instead
        self.prio_channel = get_target_product_channel(default=None)

        self.cloud_provider = cloud_provider

        # Cloud provider might have multiple variants, e.g, aws: (aws, aws-sap-es4) - normalize it
        cloud_providers = ('aws', 'azure', 'google')
        for provider in cloud_providers:
            if cloud_provider.startswith(provider):
                self.cloud_provider = provider
                break

    def set_default_channels(self, default_channels):
        """
        Set the default channels that are used as a fallback when searching
        for the right target repository.

        Usually it's not problem to find a target repository that matches
        the source repository, however in some cases the target repository
        doesn't have to be available in the required (premium) channel but
        could be present in the standard one.

        This is used usually for fallbacks and it's prerequisite for the time
        the required channel will not be present for the particular repository.
        E.g. can happen for layered products which has different lifecycles
        and doesn't have to provide a special premium channels at all. E.g.
        the Extras repository has GA and Beta channels, but no EUS. In case the
        EUS is required, returns the GA one instead when this is present.

        It's recommended to make the GA ('ga') always present in the default
        list.

        :param default_channels: Default channels to use when a target equivalent to a source repository that
                                 matches its target channel properties exactly could not be found.
        :type default_channels: List[str]
        """
        self.default_channels = default_channels

    def get_pesid_repo_entry(self, repoid, major_version):
        """
        Retrieve the PESIDRepositoryEntry that matches the given repoid and OS major version.

        If multiple pesid repo entries with the same repoid were found, the entry with rhui matching the source
        system's rhui info will be returned. If no entry with matching rhui exists, the CDN one is returned if any.

        :param repoid: RepoID that should the PESIDRepositoryEntry match.
        :type repoid: str
        :param major_version: RepoID that should the PESIDRepositoryEntry match.
        :type major_version: str
        :return: The PESIDRepositoryEntry matching the given repoid and major_version or None if no such
                 entry could be found.
        :rtype: Optional[PESIDRepositoryEntry]
        """
        matching_pesid_repos = []
        for pesid_repo in self.repositories:
            if pesid_repo.repoid == repoid and pesid_repo.major_version == major_version:
                matching_pesid_repos.append(pesid_repo)

        if len(matching_pesid_repos) == 1:
            # Perform no heuristics if only a single pesid repository with matching repoid found
            return matching_pesid_repos[0]

        # Multiple (different) repositories with the same repoid found (can happen in clouds) - prefer the cloud one
        cdn_pesid_repo = None
        for pesid_repo in matching_pesid_repos:
            if pesid_repo.rhui == self.cloud_provider:
                return pesid_repo
            if not pesid_repo.rhui:
                cdn_pesid_repo = pesid_repo

        # If we did not find a repoid for the current cloud provider, return the CDN repository
        return cdn_pesid_repo  # might be None e.g. if we are on Azure with an AWS repository enabled (unlikely)

    def get_target_pesids(self, source_pesid):
        """
        Return sorted list of target PES IDs for the given source PES ID.

        :param source_pesid: Source PES ID to find equivalents for.
        :type source_pesid: src
        :return: The list of target PES IDs the provided source_pesid is mapped to.
        :rtype: List[PESIDRepositoryEntry]
        """
        pesids = set()
        for repomap in self.mapping:
            if repomap.source == source_pesid:
                pesids.update(repomap.target)
        return sorted(pesids)

    def get_pesid_repos(self, pesid, major_version):
        """
        Get the list of PESIDRepositoryEntry with the specified PES ID and OS major version.

        :param pesid: PES ID of the repositories to be retrieved.
        :type pesid: str
        :param major_version: OS major version of the repositories to be retrieved.
        :type major_version: str
        :return: A list of PESIDRepositoryEntries that match the provided PES ID and OS major version.
        :rtype: List[PESIDRepositoryEntry]
        """
        pesid_repos = []
        for pesid_repo in self.repositories:
            if pesid_repo.pesid == pesid and pesid_repo.major_version == major_version:
                pesid_repos.append(pesid_repo)
        return pesid_repos

    def get_source_pesid_repos(self, pesid):
        """
        Return the list of PESIDRepositoryEntry objects for a specified PES ID
        matching the source OS major version.

        :param pesid: The PES ID for which to retrieve PESIDRepositoryEntries.
        :type pesid: str
        :return: A list of PESIDRepositoryEntries that match the provided PES ID and have
                 the OS Major version same as the source OS.
        :rtype: List[PESIDRepositoryEntry]
        """
        return self.get_pesid_repos(pesid, get_source_major_version())

    def get_target_pesid_repos(self, pesid):
        """
        Return the list of PESIDRepositoryEntry objects for a specified PES ID
        matching the target OS major version.

        :param pesid: The PES ID for which to retrieve PESIDRepositoryEntries.
        :type pesid: str
        :return: A list of PESIDRepositoryEntries that match the provided PES ID and have
                 the OS Major version same as the target OS.
        :rtype: List[PESIDRepositoryEntry]
        """
        return self.get_pesid_repos(pesid, get_target_major_version())

    def _find_repository_target_equivalent(self, src_pesidrepo, target_pesid):
        """
        Find the target repository that is the best-match to the source one with the given
        target PES ID.

        :param src_pesidrepo: The source repository to find equivalent to.
        :type src_pesidrepo: PESIDRepositoryEntry
        :param target_pesid: The target PES ID which the target repository must contain.
        :type target_pesid: str
        :return: A target equivalent of given repository.
        :rtype: Optional[PESIDRepositoryEntry]
        """

        candidates = []
        for candidate in self.get_target_pesid_repos(target_pesid):
            matches_rhui = candidate.rhui == src_pesidrepo.rhui
            matches_repo_type = candidate.repo_type == 'rpm'
            matches_arch = candidate.arch == api.current_actor().configuration.architecture

            if matches_rhui and matches_arch and matches_repo_type:
                # user can specify in future the specific channel should be
                # prioritized always (e.g. want to go to EUS...).
                channel = self.prio_channel or src_pesidrepo.channel
                if candidate.channel == channel:
                    return candidate
                candidates.append(candidate)

        # Fallback...
        # Could not find exact-match, so go through candidates if we find an
        # alternative in one of default channels (usually just 'ga')
        for channel in self.default_channels:
            for candidate in candidates:
                if channel == candidate.channel:
                    return candidate

        # This is a case, that must be handled by the caller
        return None

    def get_mapped_target_pesid_repos(self, src_pesidrepo):
        """
        Returns the dict of form {target_pesid: target PESIDRepositoryEntry} containing
        pesids of repositories that is the source pesidrepo mapped to as keys and with
        the best fitting repository for each of the target pesids as values.

        The function always returns the best candidate for every target pesid
        that fits to the given source repository. In case no repository is find
        for a pesid, the value in dict is None: {pesid: None}

        :param src_pesidrepo: The PESIDRepositoryEntry to find the target pesids and the corresponding
                              best-fitting repositories to.
        :type src_pesidrepo: PESIDRepositoryEntry
        :return: A dictionary of the form described above.
        :rtype: Dict[str, PESIDRepositoryEntry]
        """
        result = {}
        for target_pesid in self.get_target_pesids(src_pesidrepo.pesid):
            result[target_pesid] = self._find_repository_target_equivalent(src_pesidrepo, target_pesid)
        return result

    def get_mapped_target_repoids(self, src_pesidrepo):
        """
        Return the list of target repoids for the given src_pesidrepo.

        Some actors do not has to check whether a target repository has been
        found for each target PES ID and details about the target repositories.
        For such actors it's ok to see just list of repoids they should be
        interested about and keep the proper checks for the right actor.

        :param src_pesidrepo: The PESIDRepositoryEntry to find the corresponding best-fitting repositories to.
        :type src_pesidrepo: PESIDRepositoryEntry
        :return: A dictionary of the form described above.
        :rtype: Dict[str, PESIDRepositoryEntry]
        """
        return [repo.repoid for repo in self.get_mapped_target_pesid_repos(src_pesidrepo).values() if repo]

    def get_expected_target_pesid_repos(self, src_repoids):
        """
        Return {target_pesid: PESIDRepositoryEntry} with expected target repositories.

        If some repositories are mapped to a target pesid for which no equivalent
        repository is discovered, such a key contains just None value.

        :param src_repoids: list of present source repoids that should be mapped to target repositories
        :type src_repoids: List[str]
        :rtype: {str: PESIDRepositoryEntry}
        """
        # {pesid: target_repo}
        target_repos_best_candidates = {}
        for src_repoid in src_repoids:
            src_pesidrepo = self.get_pesid_repo_entry(src_repoid, get_source_major_version())
            if not src_pesidrepo:
                # unmapped or custom repo -> skip this one
                continue

            for target_pesid, target_candidate in self.get_mapped_target_pesid_repos(src_pesidrepo).items():
                best_candidate = target_repos_best_candidates.get(target_pesid, None)
                if not best_candidate:
                    # we need to initialize the pesid even when the target_candidate is empty
                    # to know we possibly miss something
                    target_repos_best_candidates[target_pesid] = target_candidate
                if not target_candidate:
                    # It's not crucial in this moment - the pesid can be still filled
                    # by other maps. However log the warning as it is still unexpected
                    # with the valid repomap data
                    api.current_logger().warning(
                        'Cannot find any mapped target repository from the'
                        ' {pesid} family for the {repoid} repository.'
                        .format(repoid=src_repoid, pesid=target_pesid)
                    )
                    continue

                if best_candidate and _get_channel_prio(target_candidate) > _get_channel_prio(best_candidate):
                    # NOTE(pstodulk): we want just one target repository from the "PESID family"
                    # priority: beta < ga < all_else
                    # Why all_else?
                    # -> we do not expect multiple different premium channels present on the system
                    target_repos_best_candidates[target_pesid] = target_candidate
        return target_repos_best_candidates


def get_default_repository_channels(repomap, src_repoids):
    """
    Returns the default repository channels. The 'ga' channel is always included and is the last
    one in the list, so it is the lowest priority when checking for channels.

    :param repomap: A RepoMapDataHandler instance containing the RepositoriesMapping data.
    :type repomap: RepoMapDataHandler
    :param src_repoids: Repositories present on the source system.
    :type src_repoids: List[str]
    :rtype: List[str]
    """
    default_pesid = DEFAULT_PESID[get_source_major_version()]
    top_prio_pesid_repo = None
    for repoid in src_repoids:
        pesid_repo = repomap.get_pesid_repo_entry(repoid, get_source_major_version())
        if not pesid_repo or pesid_repo.pesid != default_pesid:
            continue
        if not top_prio_pesid_repo or _get_channel_prio(pesid_repo) > _get_channel_prio(top_prio_pesid_repo):
            top_prio_pesid_repo = pesid_repo

    # always return at least 'ga'
    if not top_prio_pesid_repo or top_prio_pesid_repo.channel == 'ga':
        return ['ga']

    # keep this order to prefer higher prio to check first
    return [top_prio_pesid_repo.channel, 'ga']


# repos/system_upgrade/common/actors/setuptargetrepos/tests/test_repomapping.py

import functools
import logging

import pytest

from leapp.libraries.actor import setuptargetrepos_repomap
from leapp.libraries.actor.setuptargetrepos_repomap import get_default_repository_channels, RepoMapDataHandler
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import PESIDRepositoryEntry, RepoMapEntry, RepositoriesMapping


def make_pesid_repo(pesid, major_version, repoid, arch='x86_64', repo_type='rpm', channel='ga', rhui=''):
    """
    PESIDRepositoryEntry factory function allowing shorter data description in tests by providing default values.
    """
    return PESIDRepositoryEntry(
        pesid=pesid,
        major_version=major_version,
        repoid=repoid,
        arch=arch,
        repo_type=repo_type,
        channel=channel,
        rhui=rhui
    )


@pytest.fixture
def repomap_data_for_pesid_repo_retrieval():
    # NOTE: do not change order or repositories; particular unit-tests tests
    # use specific repositories for the comparison, expecting them on the right
    # position in the list
    repomap_data = RepositoriesMapping(
        mapping=[
            RepoMapEntry(source='pesid1', target=['pesid3', 'pesid2']),
            RepoMapEntry(source='pesid4', target=['pesid4']),
        ],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid'),
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-eus', channel='eus'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid'),
            make_pesid_repo('pesid3', '8', 'pesid3-repoid'),
            make_pesid_repo('pesid3', '8', 'pesid3-repoid-eus', channel='eus'),
            make_pesid_repo('pesid3', '8', 'pesid3-repoid-aws', rhui='aws'),
            make_pesid_repo('pesid4', '7', 'pesid4-repoid1'),
            make_pesid_repo('pesid4', '8', 'pesid4-repoid2'),
        ]
    )
    return repomap_data


def test_get_pesid_repo_entry(monkeypatch, repomap_data_for_pesid_repo_retrieval):
    """
    Test for the RepoMapDataHandler.get_pesid_repo_entry method.

    Verifies that the method correctly retrieves PESIDRepositoryEntry that are matching the OS major version
    and repoid.
    """
    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    repositories = repomap_data_for_pesid_repo_retrieval.repositories
    handler = RepoMapDataHandler(repomap_data_for_pesid_repo_retrieval)

    fail_description = (
        'get_pesid_repo_entry method failed to find correct pesid repository that matches given parameters.')
    for exp_repo in repositories:
        result_repo = handler.get_pesid_repo_entry(exp_repo.repoid, exp_repo.major_version)
        assert result_repo == exp_repo, fail_description

    fail_description = (
        'get_pesid_repo_entry method found a pesid repository, but no repository should match given parameters.')
    assert handler.get_pesid_repo_entry('pesid1-repoid', '6') is None, fail_description
    assert handler.get_pesid_repo_entry('pesid1-repoid', '8') is None, fail_description
    assert handler.get_pesid_repo_entry('pesid1-repoid', '9') is None, fail_description
    assert handler.get_pesid_repo_entry('nonexisting-repo', '7') is None, fail_description


def test_get_target_pesids(monkeypatch, repomap_data_for_pesid_repo_retrieval):
    """
    Test for the RepoMapDataHandler.get_target_pesids method.

    Verifies that the method correctly tells what target pesids is the given source pesid mapped to.
    """
    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    handler = RepoMapDataHandler(repomap_data_for_pesid_repo_retrieval)

    expected_target_pesids = ['pesid2', 'pesid3']
    actual_target_pesids = handler.get_target_pesids('pesid1')

    fail_description = (
        'The get_target_pesids method did not correctly identify what is the given source pesid mapped to.')
    assert expected_target_pesids == actual_target_pesids, fail_description

    fail_description = (
        'The get_target_pesids method found target pesids even if the source repository is not mapped.')
    assert [] == handler.get_target_pesids('pesid2'), fail_description
    assert [] == handler.get_target_pesids('pesid_no_mapping'), fail_description


def test_get_pesid_repos(monkeypatch, repomap_data_for_pesid_repo_retrieval):
    """
    Test for the RepoMapDataHandler.get_pesid_repos method.

    Verifies that the method is able to collect all PESIDRepositoryEntry present in the repomap data that
    match the given OS major version and the given pesid.
    """
    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    handler = RepoMapDataHandler(repomap_data_for_pesid_repo_retrieval)
    repositories = repomap_data_for_pesid_repo_retrieval.repositories

    actual_pesid_repos = handler.get_pesid_repos('pesid3', '8')
    expected_pesid_repos = [repositories[3], repositories[4], repositories[5]]
    fail_description = 'The get_pesid_repos failed to find pesid repos matching the given criteria.'
    assert len(expected_pesid_repos) == len(actual_pesid_repos), fail_description
    for actual_pesid_repo in actual_pesid_repos:
        assert actual_pesid_repo in expected_pesid_repos, fail_description

    actual_pesid_repos = handler.get_pesid_repos('pesid1', '7')
    expected_pesid_repos = [repositories[0], repositories[1]]
    assert len(expected_pesid_repos) == len(actual_pesid_repos), fail_description
    for actual_pesid_repo in actual_pesid_repos:
        assert actual_pesid_repo in expected_pesid_repos, fail_description

    fail_description = (
        'The get_pesid_repos found some pesid repositories matching criteria, but there are no such repositories.')
    assert [] == handler.get_pesid_repos('pesid3', '7'), fail_description
    assert [] == handler.get_pesid_repos('pesid1', '8'), fail_description
    assert [] == handler.get_pesid_repos('nonexisting_pesid', '7'), fail_description


def test_get_source_pesid_repos(monkeypatch, repomap_data_for_pesid_repo_retrieval):
    """
    Test for the RepoMapDataHandler.get_source_pesid_repos method.

    Verifies that the method is able to collect all PESIDRepositoryEntry that match the given PES ID and
    have the major version same as the source system.
    """
    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    handler = RepoMapDataHandler(repomap_data_for_pesid_repo_retrieval)
    repositories = repomap_data_for_pesid_repo_retrieval.repositories

    fail_description = (
        'The get_source_pesid_repos method failed to retrieve all pesid repos that match given pesid '
        'and have the major version same as the source system.')
    expected_pesid_repos = [repositories[0], repositories[1]]
    actual_pesid_repos = handler.get_source_pesid_repos('pesid1')
    assert len(expected_pesid_repos) == len(actual_pesid_repos), fail_description
    for actual_pesid_repo in actual_pesid_repos:
        assert actual_pesid_repo in expected_pesid_repos, fail_description

    fail_description = (
        'The get_source_pesid_repos method does not take into account the source system version correctly.'
    )
    monkeypatch.setattr(setuptargetrepos_repomap, 'get_source_major_version', lambda: '10')

    # Repeat the same test as above to make sure it respects the source OS major version
    assert [] == handler.get_source_pesid_repos('pesid1'), fail_description

    assert [] == handler.get_source_pesid_repos('pesid2'), fail_description
    assert [] == handler.get_source_pesid_repos('nonexisting_pesid'), fail_description


def test_get_target_pesid_repos(monkeypatch, repomap_data_for_pesid_repo_retrieval):
    """
    Test for the RepoMapDataHandler.get_target_pesid_repos method.

    Verifies that the method is able to collect all PESIDRepositoryEntry that match the given PES ID and
    have the major version same as the source system.
    """
    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    handler = RepoMapDataHandler(repomap_data_for_pesid_repo_retrieval)
    repositories = repomap_data_for_pesid_repo_retrieval.repositories

    fail_description = (
        'The get_target_pesid_repos method failed to retrieve all pesid repos that match given pesid '
        'and have the major version same as the target system.')
    expected_pesid_repos = [repositories[3], repositories[4], repositories[5]]
    actual_pesid_repos = handler.get_target_pesid_repos('pesid3')
    assert len(expected_pesid_repos) == len(actual_pesid_repos), fail_description
    for actual_pesid_repo in actual_pesid_repos:
        assert actual_pesid_repo in expected_pesid_repos, fail_description

    fail_description = (
        'The get_target_pesid_repos method doesn\'t take into account the target system version correctly.'
    )
    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='9.4', dst_ver='10.0'))

    # Repeat the same test as above to make sure it respects the target OS major version
    assert [] == handler.get_target_pesid_repos('pesid3'), fail_description

    assert [] == handler.get_target_pesid_repos('pesid1'), fail_description
    assert [] == handler.get_target_pesid_repos('nonexisting_pesid'), fail_description


@pytest.fixture
def mapping_data_for_find_repository_equiv():
    repositories = [
        make_pesid_repo('pesid1', '7', 'pesid1-repoid'),
        make_pesid_repo('pesid1', '7', 'pesid1-repoid', channel='e4s', rhui='aws'),
        make_pesid_repo('pesid2', '8', 'pesid2-repoid1'),
        make_pesid_repo('pesid2', '8', 'pesid2-repoid2-s390x', arch='s390x'),
        # This repository is a better candidate than the full match equivalent, but _find_repository_target_equivalent
        # should not take into account channel priorities
        make_pesid_repo('pesid2', '8', 'pesid2-repoid2', arch='x86_64', channel='eus'),
        make_pesid_repo('pesid2', '8', 'pesid2-repoid3-srpm', repo_type='srpm'),
        make_pesid_repo('pesid2', '8', 'pesid2-repoid4.1', rhui='aws'),
        make_pesid_repo('pesid2', '8', 'pesid2-repoid4.2', channel='eus', rhui='aws'),
        make_pesid_repo('pesid3', '8', 'pesid3-repoid')
    ]
    return RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2', 'pesid3'])],
        repositories=repositories
    )


def test_find_repository_target_equivalent_fullmatch(monkeypatch, mapping_data_for_find_repository_equiv):
    """
    Test for the RepoMapDataHandler._find_repository_target_equivalent method.

    Verifies that the method can find the target equivalent for a repository that matches the source
    pesid repo parameters exactly when such repository is available in the repository mapping data.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    handler = RepoMapDataHandler(mapping_data_for_find_repository_equiv)

    expected_target_equivalent = mapping_data_for_find_repository_equiv.repositories[2]
    actual_target_equivalent = handler._find_repository_target_equivalent(
        mapping_data_for_find_repository_equiv.repositories[0], 'pesid2')

    fail_description = (
        'The _find_repository_target_equivalent failed to find equivalent that exactly matched the source pesid repo '
        'when there is such equivalent available in the repository mapping data.')
    assert expected_target_equivalent == actual_target_equivalent, fail_description


def test_find_repository_target_equivalent_fallback_to_default(monkeypatch,
                                                               mapping_data_for_find_repository_equiv):
    """
    Test for the RepoMapDataHandler._find_repository_target_equivalent method.

    Verifies that the method will find a target equivalent with matching some of the fallback
    channels if a target equivalent that matches the source pesid repository completely is not
    available in the repository mapping data.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    handler = RepoMapDataHandler(mapping_data_for_find_repository_equiv)
    repositories = mapping_data_for_find_repository_equiv.repositories

    fail_description = (
        'The _find_repository_target_equivalent failed to find repository with some of the fallback channels.')
    expected_target_equivalent = repositories[6]
    actual_target_equivalent = handler._find_repository_target_equivalent(repositories[1], 'pesid2')
    assert expected_target_equivalent == actual_target_equivalent, fail_description

    handler.set_default_channels(['eus', 'ga'])

    expected_target_equivalent = repositories[7]
    actual_target_equivalent = handler._find_repository_target_equivalent(repositories[1], 'pesid2')
    assert expected_target_equivalent == actual_target_equivalent, fail_description


def test_find_repository_target_equivalent_no_target_equivalent(monkeypatch,
                                                                mapping_data_for_find_repository_equiv):
    """
    Test for the RepoMapDataHandler._find_repository_target_equivalent method.

    Verifies that the  does not crash when there is no target repository that is equivalent to the
    source repository.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='s390x', src_ver='7.9', dst_ver='8.4'))
    handler = RepoMapDataHandler(mapping_data_for_find_repository_equiv)

    fail_description = 'The _find_repository_target_equivalent found target equivalent when there are none.'

    repository_with_no_equivalent = make_pesid_repo('pesid1', '7', 'pesid1-some-repoid', arch='s390x', rhui='aws')
    target_equivalent = handler._find_repository_target_equivalent(repository_with_no_equivalent, 'pesid2')
    assert target_equivalent is None, fail_description


def test_get_mapped_target_pesid_repos(monkeypatch, mapping_data_for_find_repository_equiv):
    """
    Test for the RepoMapDataHandler.get_mapped_target_pesid_repos method.

    Verifies that the method correctly builds a map mapping the target pesid to the best candidate
    pesid repos.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    repositories = mapping_data_for_find_repository_equiv.repositories

    handler = RepoMapDataHandler(mapping_data_for_find_repository_equiv)

    # Both pesid2 and pesid3 have an equivalent for provided source pesid repository
    fail_description = (
        'The get_mapped_target_pesid_repos failed to build a map mapping the target pesid '
        'to the best pesid repository candidate.')
    target_pesid_repos_map = handler.get_mapped_target_pesid_repos(repositories[0])
    expected_pesid_to_best_candidate_map = {'pesid2': repositories[2], 'pesid3': repositories[8]}
    assert target_pesid_repos_map == expected_pesid_to_best_candidate_map, fail_description

    # The pesid3 does not have an equivalent for provided source pesid repository (due to not having any rhui repos)
    target_pesid_repos_map = handler.get_mapped_target_pesid_repos(repositories[1])
    expected_pesid_to_best_candidate_map = {'pesid2': repositories[6], 'pesid3': None}
    assert target_pesid_repos_map == expected_pesid_to_best_candidate_map, fail_description


def test_get_mapped_target_repoids(monkeypatch, mapping_data_for_find_repository_equiv):
    """
    Test for the RepoMapDataHandler.get_mapped_target_repoids method.

    Verifies that the method returns a correct list of repoids that should be present on the target system.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    handler = RepoMapDataHandler(mapping_data_for_find_repository_equiv)
    repositories = mapping_data_for_find_repository_equiv.repositories

    fail_description = (
        'The get_mapped_target_repoids method failed to return the correct list of repoids ',
        'to be enabled on the target system.')
    # Both pesid2 and pesid3 have an equivalent for provided source pesid repository
    actual_target_repoids = handler.get_mapped_target_repoids(repositories[0])
    expected_target_repoids = {repositories[2].repoid, repositories[8].repoid}
    assert len(actual_target_repoids) == len(expected_target_repoids), fail_description
    assert set(actual_target_repoids) == expected_target_repoids, fail_description

    # The pesid3 does not have an equivalent for provided source pesid
    # repository -> only one repository should be in the produced list
    actual_target_repoids = handler.get_mapped_target_repoids(repositories[1])
    assert len(actual_target_repoids) == 1
    assert 'pesid2-repoid4.1' in actual_target_repoids, fail_description


def test_get_expected_target_repoids_simple(monkeypatch):
    """
    Test for the RepoMapDataHandler.get_expected_target_repoids method.

    Verifies that the method is able to produce a correct map that maps target pesid to the best
    candidate pesid repository when there is only one repoid enabled and the corresponding source
    pesid repository has exact match target equivalent.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid')
        ]
    )
    fail_description = 'Failed to get_expected_target_repoids with only one repository enabled on the source system.'
    handler = RepoMapDataHandler(repositories_mapping)
    target_repoids = handler.get_expected_target_pesid_repos(['pesid1-repoid'])

    assert {'pesid2': repositories_mapping.repositories[1]} == target_repoids, fail_description


def test_get_expected_target_repoids_best_candidate_produced(monkeypatch):
    """
    Test for the RepoMapDataHandler.get_expected_target_repoids method.

    Verifies that the method is able to produce a correct map that maps target pesid to the best
    candidate pesid repository when there are two repositories with different priority channels
    belonging to the same pesid family enabled on the source system and both have target
    equivalents.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-ga'),
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-eus', channel='eus'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-eus', channel='eus'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-e4s', channel='e4s'),
        ]
    )

    handler = RepoMapDataHandler(repositories_mapping)
    target_repoids = handler.get_expected_target_pesid_repos(['pesid1-repoid-eus'])

    fail_description = (
        'The get_expected_target_repoids failed to map target pesid to a pesid repository'
        'with the highest priority channel.'
    )
    assert {'pesid2': repositories_mapping.repositories[3]} == target_repoids, fail_description


def test_get_expected_target_repoids_fallback(monkeypatch):
    """
    Test for the RepoMapDataHandler.get_expected_target_repoids method.

    Verifies that the RepoMapDataHandler.get_expected_target_repoids method is able to produce a correct
    map that maps target pesid to the best candidate pesid repository when there is a repository
    on the source system that does not have exact match equivalent and some other with a fallback channel
    must be found.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-eus', channel='eus'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-ga'),
            make_pesid_repo('pesid1', '8', 'pesid2-repoid-e4s', channel='e4s'),
        ]
    )

    fail_description = (
        'The get_expected_target_repoids failed to find repository with a failback channel '
        'since there were no exact target equivalents.')

    handler = RepoMapDataHandler(repositories_mapping)
    handler.set_default_channels(['ga'])
    target_repoids = handler.get_expected_target_pesid_repos(['pesid1-repoid-eus'])

    assert {'pesid2': repositories_mapping.repositories[1]} == target_repoids, fail_description


def test_get_expected_target_pesid_repos_multiple_repositories(monkeypatch):
    """
    Test for the RepoMapDataHandler.get_expected_target_repoids method.

    Verifies that the RepoMapDataHandler.get_expected_target_repoids method is able to produce a correct
    map that maps target pesid to the best candidate pesid repository when one source pesid is mapped
    to multiple target pesids (every target pesid should have an entry in the returned map).
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2', 'pesid3'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-ga'),
            make_pesid_repo('pesid3', '8', 'pesid3-repoid-ga')
        ]
    )

    fail_description = 'Failed to get_expected_target_repoids when one source pesid is mapped to two target pesids.'
    handler = RepoMapDataHandler(repositories_mapping)
    target_repoids = handler.get_expected_target_pesid_repos(['pesid1-repoid-ga'])

    assert {'pesid2': repositories_mapping.repositories[1],
            'pesid3': repositories_mapping.repositories[2]} == target_repoids, fail_description


def test_get_expected_target_pesid_repos_unmapped_repository(monkeypatch):
    """
    Test for the RepoMapDataHandler.get_expected_target_repoids method.

    Verifies that the RepoMapDataHandler.get_expected_target_repoids method does not fail
    when there is a repository on the source system that is not mapped.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-ga')
        ])

    fail_description = 'Failed to get_expected_target_repoids when one of the source repoids is unmapped.'
    handler = RepoMapDataHandler(repositories_mapping)
    target_repoids = handler.get_expected_target_pesid_repos(['pesid1-repoid-ga', 'unmapped-repoid'])

    assert {'pesid2': repositories_mapping.repositories[1]} == target_repoids, fail_description


def test_get_expected_target_pesid_repos_repo_with_no_equivalent(monkeypatch, caplog):
    """
    Test for the RepoMapDataHandler.get_expected_target_repoids method.

    Verifies that the RepoMapDataHandler.get_expected_target_repoids method does not fail
    when there is a repository on the source system that does not have any equivalents.
    A warning should be produced when a situation like this occurs.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))

    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-eus', channel='eus'),
        ]
    )

    handler = RepoMapDataHandler(repositories_mapping)
    target_repoids = handler.get_expected_target_pesid_repos(['pesid1-repoid-ga'])

    fail_description = (
        'Failed get_expected_target_repoids with a source repository that does not have any target equivalent.')
    assert {'pesid2': None} == target_repoids, fail_description
    missing_target_equivalent_message = (
        'Cannot find any mapped target repository from the pesid2 family for the pesid1-repoid-ga repository.'
    )

    # A warning should be produced when a target equivalent was not found.
    warning_produced = False
    for record in caplog.records:
        if record.levelno == logging.WARNING and record.message == missing_target_equivalent_message:
            warning_produced = True
            break
    assert warning_produced, 'A warning should be produced when a repository has no equivalent.'


def test_get_default_repository_channels_simple(monkeypatch):
    """
    Test for the get_default_repository_channels function.

    Verifies that the function returns correct list of default channels on a source system
    where there is only one repository enabled from the pesid family in which are
    the default repositories searched in.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    repository_mapping = RepositoriesMapping(
        mapping=[],
        repositories=[make_pesid_repo('rhel7-base', '7', 'rhel7-repoid-ga', channel='ga')]
    )
    handler = RepoMapDataHandler(repository_mapping)

    assert ['ga'] == get_default_repository_channels(handler, ['rhel7-repoid-ga'])


def test_get_default_repository_channels_highest_priority_channel(monkeypatch):
    """
    Test for the get_default_repository_channels function.

    Verifies that the returned list contains the highest priority channel if there is a repository
    with the channel enabled on the source system.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    repository_mapping = RepositoriesMapping(
        mapping=[],
        repositories=[
            make_pesid_repo('rhel7-base', '7', 'rhel7-repoid-ga', channel='ga'),
            make_pesid_repo('rhel7-base', '7', 'rhel7-repoid-eus', channel='eus'),
        ]
    )
    handler = RepoMapDataHandler(repository_mapping)

    assert ['eus', 'ga'] == get_default_repository_channels(handler, ['rhel7-repoid-ga', 'rhel7-repoid-eus'])


def test_get_default_repository_channels_no_default_pesid_repo(monkeypatch):
    """
    Test for the get_default_repository_channels function.

    Verifies that the returned list contains some fallback channel even if no repository from the default
    pesid family in which are the channels searched is enabled.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4'))
    repository_mapping = RepositoriesMapping(
        mapping=[],
        repositories=[
            make_pesid_repo('rhel7-base', '7', 'rhel7-repoid-ga', channel='ga'),
            make_pesid_repo('rhel7-base', '7', 'rhel7-repoid-eus', channel='eus'),
        ]
    )
    handler = RepoMapDataHandler(repository_mapping)

    assert ['ga'] == get_default_repository_channels(handler, ['some-repoid'])


def test_find_repository_equivalent_with_priority_channel(monkeypatch):
    """
    Tests whether the _find_repository_target_equivalent correctly respects the chosen preferred channel.
    """
    envars = {'LEAPP_DEVEL_TARGET_PRODUCT_TYPE': 'eus'}

    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4', envars=envars))
    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-ga', channel='ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-eus', channel='eus'),
        ]
    )

    handler = RepoMapDataHandler(repositories_mapping)
    handler.set_default_channels(['ga'])

    assert handler.prio_channel == 'eus'

    fail_description = '_find_repository_target_equivalent does not correctly respect preferred channel.'
    expected_target_equivalent = repositories_mapping.repositories[2]
    actual_target_equivalent = handler._find_repository_target_equivalent(repositories_mapping.repositories[0],
                                                                          'pesid2')
    assert expected_target_equivalent == actual_target_equivalent, fail_description


def test_get_expected_target_pesid_repos_with_priority_channel_set(monkeypatch):
    """
    Tests whether the get_expected_target_peid_repos correctly respects the chosen preferred channel.
    """

    envars = {'LEAPP_DEVEL_TARGET_PRODUCT_TYPE': 'eus'}

    monkeypatch.setattr(api, 'current_actor',
                        CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.4', envars=envars))

    repositories_mapping = RepositoriesMapping(
        mapping=[RepoMapEntry(source='pesid1', target=['pesid2', 'pesid3'])],
        repositories=[
            make_pesid_repo('pesid1', '7', 'pesid1-repoid-ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-ga'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-eus', channel='eus'),
            make_pesid_repo('pesid2', '8', 'pesid2-repoid-tuv', channel='tuv'),
            make_pesid_repo('pesid3', '8', 'pesid3-repoid-ga')
        ]
    )

    handler = RepoMapDataHandler(repositories_mapping)
    # Set defaults to verify that the priority channel is not overwritten by defaults
    handler.set_default_channels(['tuv', 'ga'])
    target_repoids = handler.get_expected_target_pesid_repos(['pesid1-repoid-ga'])

    fail_description = 'get_expected_target_peid_repos does not correctly respect preferred channel.'
    assert {'pesid2': repositories_mapping.repositories[2],
            'pesid3': repositories_mapping.repositories[4]} == target_repoids, fail_description


@pytest.mark.parametrize('rhui', ('', 'aws', 'aws-sap-e4s', 'azure', 'azure-sap'))
def test_multiple_repoids_in_repomapping(monkeypatch, rhui):
    """
    Tests whether a correct repository is selected when running on cloud with multiple repositories having the same ID.

    In such a case, the actor should use the cloud provider as a guide on which of the repositores should it pick.
    """

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(arch='x86_64', src_ver='7.9', dst_ver='8.6'))

    mk_rhui_el7_pesid_repo = functools.partial(PESIDRepositoryEntry,
                                               pesid='rhel7-rhui',
                                               major_version='7',
                                               repoid='repoid7-rhui',
                                               repo_type='rpm',
                                               arch='x86_64',
                                               channel='ga')

    mk_rhui_el8_pesid_repo = functools.partial(PESIDRepositoryEntry,
                                               pesid='rhel8-rhui',
                                               major_version='8',
                                               repo_type='rpm',
                                               arch='x86_64',
                                               channel='ga')

    repomap = RepositoriesMapping(
        mapping=[RepoMapEntry(source='rhel7-rhui', target=['rhel8-rhui'])],
        repositories=[
            mk_rhui_el7_pesid_repo(rhui=''),
            mk_rhui_el7_pesid_repo(rhui='aws'),
            mk_rhui_el7_pesid_repo(rhui='azure'),
            mk_rhui_el8_pesid_repo(repoid='repoid8-rhui', rhui=''),
            mk_rhui_el8_pesid_repo(repoid='repoid8-rhui-aws', rhui='aws'),
            mk_rhui_el8_pesid_repo(repoid='repoid8-rhui-azure', rhui='azure'),
        ]
    )

    handler = RepoMapDataHandler(repomap, cloud_provider=rhui)
    target_repoids = handler.get_expected_target_pesid_repos(['repoid7-rhui'])

    assert len(target_repoids) == 1

    expected_suffixes = {
        '': '',
        'aws': '-aws',
        'aws-sap-e4s': '-aws',
        'azure': '-azure',
        'azure-sap': '-azure'
    }

    assert 'rhel8-rhui' in target_repoids
    assert target_repoids['rhel8-rhui'].repoid == 'repoid8-rhui{0}'.format(expected_suffixes[rhui])


# repos/system_upgrade/common/actors/setuptargetrepos/tests/test_setuptargetrepos.py

import pytest

from leapp.libraries import stdlib
from leapp.libraries.actor import setuptargetrepos
from leapp.libraries.common.testutils import CurrentActorMocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
    CustomTargetRepository,
    PESIDRepositoryEntry,
    RepoMapEntry,
    RepositoriesBlacklisted,
    RepositoriesFacts,
    RepositoriesMapping,
    RepositoriesSetupTasks,
    RepositoryData,
    RepositoryFile,
    TargetRepositories
)


def test_minimal_execution(monkeypatch):
    """
    Tests whether the actor does not fail if no messages except the RepositoriesMapping are provided.
    """
    msgs = [
        RepositoriesMapping(mapping=[], repositories=[])
    ]

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    setuptargetrepos.process()


def test_custom_repos(monkeypatch):
    """
    Tests whether the CustomRepos provided to the actor are propagated to the TargetRepositories after
    blacklist filtering is applied on them.
    """
    custom = CustomTargetRepository(repoid='rhel-8-server-rpms',
                                    name='RHEL 8 Server (RPMs)',
                                    baseurl='https://.../dist/rhel/server/8/os',
                                    enabled=True)

    blacklisted = CustomTargetRepository(repoid='rhel-8-blacklisted-rpms',
                                         name='RHEL 8 Blacklisted (RPMs)',
                                         baseurl='https://.../dist/rhel/blacklisted/8/os',
                                         enabled=True)

    repos_blacklisted = RepositoriesBlacklisted(repoids=['rhel-8-blacklisted-rpms'])

    repositories_mapping = RepositoriesMapping(
        mapping=[],
        repositories=[]
    )

    msgs = [custom, blacklisted, repos_blacklisted, repositories_mapping]

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    setuptargetrepos.process()

    assert api.produce.called

    custom_repos = api.produce.model_instances[0].custom_repos
    assert len(custom_repos) == 1
    assert custom_repos[0].repoid == 'rhel-8-server-rpms'


def test_repositories_setup_tasks(monkeypatch):
    """
    Tests whether the actor propagates repositories received via a RepositoriesSetupTasks message
    to the resulting TargetRepositories (and blacklist filtering is applied to them).
    """
    repositories_setup_tasks = RepositoriesSetupTasks(to_enable=['rhel-8-server-rpms',
                                                                 'rhel-8-blacklisted-rpms'])
    repos_blacklisted = RepositoriesBlacklisted(repoids=['rhel-8-blacklisted-rpms'])
    repositories_mapping = RepositoriesMapping(mapping=[], repositories=[])
    msgs = [repositories_setup_tasks, repos_blacklisted, repositories_mapping]

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    setuptargetrepos.process()

    assert api.produce.called

    rhel_repos = api.produce.model_instances[0].rhel_repos
    assert len(rhel_repos) == 1
    assert rhel_repos[0].repoid == 'rhel-8-server-rpms'


def test_repos_mapping(monkeypatch):
    """
    Tests whether actor correctly determines what repositories should be enabled on target based
    on the information about what repositories are enabled on the source system using
    the RepositoriesMapping information.
    """
    repos_data = [
        RepositoryData(repoid='rhel-7-server-rpms', name='RHEL 7 Server'),
        RepositoryData(repoid='rhel-7-blacklisted-rpms', name='RHEL 7 Blacklisted')]

    repos_files = [RepositoryFile(file='/etc/yum.repos.d/redhat.repo', data=repos_data)]
    facts = RepositoriesFacts(repositories=repos_files)

    repomap = RepositoriesMapping(
        mapping=[RepoMapEntry(source='rhel7-base', target=['rhel8-baseos', 'rhel8-appstream', 'rhel8-blacklist'])],
        repositories=[
            PESIDRepositoryEntry(
                pesid='rhel7-base',
                repoid='rhel-7-server-rpms',
                major_version='7',
                arch='x86_64',
                repo_type='rpm',
                channel='ga',
                rhui=''
            ),
            PESIDRepositoryEntry(
                pesid='rhel8-baseos',
                repoid='rhel-8-for-x86_64-baseos-htb-rpms',
                major_version='8',
                arch='x86_64',
                repo_type='rpm',
                channel='ga',
                rhui=''
            ),
            PESIDRepositoryEntry(
                pesid='rhel8-appstream',
                repoid='rhel-8-for-x86_64-appstream-htb-rpms',
                major_version='8',
                arch='x86_64',
                repo_type='rpm',
                channel='ga',
                rhui=''
            ),
            PESIDRepositoryEntry(
                pesid='rhel8-blacklist',
                repoid='rhel-8-blacklisted-rpms',
                major_version='8',
                arch='x86_64',
                repo_type='rpm',
                channel='ga',
                rhui=''
            ),
        ]
    )

    repos_blacklisted = RepositoriesBlacklisted(repoids=['rhel-8-blacklisted-rpms'])

    msgs = [facts, repomap, repos_blacklisted]

    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
    monkeypatch.setattr(api, 'produce', produce_mocked())

    setuptargetrepos.process()
    assert api.produce.called

    rhel_repos = api.produce.model_instances[0].rhel_repos
    assert len(rhel_repos) == 2

    produced_rhel_repoids = {repo.repoid for repo in rhel_repos}
    expected_rhel_repoids = {'rhel-8-for-x86_64-baseos-htb-rpms', 'rhel-8-for-x86_64-appstream-htb-rpms'}
    assert produced_rhel_repoids == expected_rhel_repoids


# repos/system_upgrade/common/actors/storagescanner/libraries/storagescanner.py

import functools
import os
import subprocess

import pyudev

from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.models import (
    FstabEntry,
    LsblkEntry,
    LvdisplayEntry,
    MountEntry,
    PartitionEntry,
    PvsEntry,
    StorageInfo,
    SystemdMountEntry,
    VgsEntry
)


def aslist(f):
    """ Decorator used to convert generator to list """
    @functools.wraps(f)
    def inner(*args, **kwargs):
        return list(f(*args, **kwargs))
    return inner


def _is_file_readable(path):
    """ Verify if file exists and is readable """
    return os.path.isfile(path) and os.access(path, os.R_OK)


def _get_cmd_output(cmd, delim, expected_len):
    """ Verify if command exists and return output """
    if not any(os.access(os.path.join(path, cmd[0]), os.X_OK) for path in os.environ['PATH'].split(os.pathsep)):
        api.current_logger().warning("'%s': command not found" % cmd[0])
        return

    try:
        # FIXME: Will keep call to subprocess until our stdlib supports "env" parameter
        # when there is any fd except 0,1,2 open, lvm closes the fd and prints a warning.
        # In our case /dev/urandom has other fd opened, probably for caching purposes.
        output = subprocess.check_output(cmd, env={'LVM_SUPPRESS_FD_WARNINGS': '1', 'PATH': os.environ['PATH']})

    except subprocess.CalledProcessError as e:
        api.current_logger().debug("Command '%s' return non-zero exit status: %s" % (" ".join(cmd), e.returncode))
        return

    if bytes is not str:
        output = output.decode('utf-8')

    for entry in output.split('\n'):
        entry = entry.strip()
        if not entry:
            continue

        data = entry.split(delim)
        data.extend([''] * (expected_len - len(data)))

        yield data


@aslist
def _get_partitions_info(partitions_path):
    """ Collect storage info from /proc/partitions file """
    if _is_file_readable(partitions_path):
        with open(partitions_path, 'r') as partitions:
            skipped_header = False
            for entry in partitions:
                if entry.startswith('#'):
                    continue

                if not skipped_header:
                    skipped_header = True
                    continue

                entry = entry.strip()
                if not entry:
                    continue

                major, minor, blocks, name = entry.split()
                yield PartitionEntry(
                    major=major,
                    minor=minor,
                    blocks=blocks,
                    name=name)


@aslist
def _get_fstab_info(fstab_path):
    """ Collect storage info from /etc/fstab file """
    if _is_file_readable(fstab_path):
        with open(fstab_path, 'r') as fstab:
            for line, entry in enumerate(fstab, 1):
                if entry.startswith('#'):
                    continue

                entry = entry.strip()
                if not entry:
                    continue

                entries = entry.split()

                if len(entries) == 4:
                    entries.append('0')

                if len(entries) == 5:
                    entries.append('0')

                if len(entries) != 6:
                    if any(value.startswith('#') for value in entries):
                        remediation = (
                            'Comments in the /etc/fstab file must be at the beginning of the line, your file has a'
                            ' comment at the end of the line at line {}, please edit and fix this, for further'
                            ' information read fstab man page (man 5 fstab).'.format(line)
                        )
                    else:
                        remediation = (
                            'The /etc/fstab file must have at least 4 values and at most 6 per line, your file on the'
                            ' line: {} have {} values, please edit and fix this, for further information read'
                            ' fstab man page (man 5 fstab). '.format(line, len(entries))
                        )
                    summary = (
                        'The fstab configuration file seems to be invalid. You need to fix it to be able to proceed'
                        ' with the upgrade process.'
                    )
                    reporting.create_report([
                        reporting.Title('Problems with parsing data in /etc/fstab'),
                        reporting.Summary(summary),
                        reporting.Severity(reporting.Severity.HIGH),
                        reporting.Groups([reporting.Groups.FILESYSTEM]),
                        reporting.Groups([reporting.Groups.INHIBITOR]),
                        reporting.Remediation(hint=remediation),
                        reporting.RelatedResource('file', '/etc/fstab')
                    ])

                    api.current_logger().error(summary)
                    break

                fs_spec, fs_file, fs_vfstype, fs_mntops, fs_freq, fs_passno = entries
                yield FstabEntry(
                    fs_spec=fs_spec,
                    fs_file=fs_file,
                    fs_vfstype=fs_vfstype,
                    fs_mntops=fs_mntops,
                    fs_freq=fs_freq,
                    fs_passno=fs_passno)


@aslist
def _get_mount_info(path):
    """ Collect storage info """
    with open(path, 'r') as fp:
        for line in [l.strip() for l in fp.readlines()]:
            device, mount, tp, options, _, _ = line.split(' ')
            yield MountEntry(
                name=device,
                mount=mount,
                tp=tp,
                options=options
            )


@aslist
def _get_lsblk_info():
    """ Collect storage info from lsblk command """
    cmd = ['lsblk', '-pbnr', '--output', 'NAME,MAJ:MIN,RM,SIZE,RO,TYPE,MOUNTPOINT']
    for entry in _get_cmd_output(cmd, ' ', 7):
        dev_path, maj_min, rm, bsize, ro, tp, mountpoint = entry
        lsblk_cmd = ['lsblk', '-nr', '--output', 'NAME,KNAME,SIZE', dev_path]
        lsblk_info_for_devpath = next(_get_cmd_output(lsblk_cmd, ' ', 3), None)
        if not lsblk_info_for_devpath:
            return

        name, kname, size = lsblk_info_for_devpath
        yield LsblkEntry(
            name=name,
            kname=kname,
            maj_min=maj_min,
            rm=rm,
            size=size,
            bsize=int(bsize),
            ro=ro,
            tp=tp,
            mountpoint=mountpoint)


@aslist
def _get_pvs_info():
    """ Collect storage info from pvs command """
    for entry in _get_cmd_output(['pvs', '--noheadings', '--separator', r':'], ':', 6):
        pv, vg, fmt, attr, psize, pfree = entry
        yield PvsEntry(
            pv=pv,
            vg=vg,
            fmt=fmt,
            attr=attr,
            psize=psize,
            pfree=pfree)


@aslist
def _get_vgs_info():
    """ Collect storage info from vgs command """
    for entry in _get_cmd_output(['vgs', '--noheadings', '--separator', r':'], ':', 7):
        vg, pv, lv, sn, attr, vsize, vfree = entry
        yield VgsEntry(
            vg=vg,
            pv=pv,
            lv=lv,
            sn=sn,
            attr=attr,
            vsize=vsize,
            vfree=vfree)


@aslist
def _get_lvdisplay_info():
    """ Collect storage info from lvdisplay command """
    for entry in _get_cmd_output(['lvdisplay', '-C', '--noheadings', '--separator', r':'], ':', 12):
        lv, vg, attr, lsize, pool, origin, data, meta, move, log, cpy_sync, convert = entry
        yield LvdisplayEntry(
            lv=lv,
            vg=vg,
            attr=attr,
            lsize=lsize,
            pool=pool,
            origin=origin,
            data=data,
            meta=meta,
            move=move,
            log=log,
            cpy_sync=cpy_sync,
            convert=convert)


@aslist
def _get_systemd_mount_info():
    """
    Collect the same storage info as provided by the systemd-mount command.

    The actual implementation no longer relies on calling the systemd-mount, but rather collects the same information
    from udev directly using pyudev. The systemd-mount output parsing has proved not to be unreliable due to
    its tabular format.
    """
    ctx = pyudev.Context()
    # Filter the devices in the same way `systemd-mount --list` does
    for device in ctx.list_devices(subsystem='block', ID_FS_USAGE='filesystem'):
        # Use 'n/a' to provide the same value for unknown output fields same way the systemd-mount does
        yield SystemdMountEntry(
            node=device.device_node,
            path=device.get('ID_PATH', default='n/a'),
            model=device.get('ID_MODEL', default='n/a'),
            wwn=device.get('ID_WWN', default='n/a'),
            fs_type=device.get('ID_FS_TYPE', default='n/a'),
            label=device.get('ID_FS_LABEL', default='n/a'),
            uuid=device.get('ID_FS_UUID', default='n/a')
        )


def get_storage_info():
    """ Collect multiple info about storage and return it """
    return StorageInfo(
        partitions=_get_partitions_info('/proc/partitions'),
        fstab=_get_fstab_info('/etc/fstab'),
        mount=_get_mount_info('/proc/mounts'),
        lsblk=_get_lsblk_info(),
        pvs=_get_pvs_info(),
        vgs=_get_vgs_info(),
        lvdisplay=_get_lvdisplay_info(),
        systemdmount=_get_systemd_mount_info())


# repos/system_upgrade/common/actors/storagescanner/tests/unit_test_storagescanner.py

import functools
import os

import pyudev

from leapp import reporting
from leapp.libraries.actor import storagescanner
from leapp.libraries.common.testutils import create_report_mocked, logger_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
    FstabEntry,
    LsblkEntry,
    LvdisplayEntry,
    MountEntry,
    PartitionEntry,
    PvsEntry,
    SystemdMountEntry,
    VgsEntry
)
from leapp.utils.report import is_inhibitor

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


def test_get_partitions_info(monkeypatch):
    def is_file_readable_mocked(path):
        return False

    expected = [
        PartitionEntry(major='252', minor='0', blocks='41943040', name='vda'),
        PartitionEntry(major='252', minor='1', blocks='1048576', name='vda1'),
        PartitionEntry(major='252', minor='2', blocks='40893440', name='vda2'),
        PartitionEntry(major='253', minor='0', blocks='39837696', name='dm-0'),
        PartitionEntry(major='253', minor='1', blocks='1048576', name='dm-1')]
    assert expected == storagescanner._get_partitions_info(os.path.join(CUR_DIR, 'files/partitions'))

    monkeypatch.setattr(storagescanner, '_is_file_readable', is_file_readable_mocked)
    assert [] == storagescanner._get_partitions_info('unreadable_file')


def test_get_fstab_info(monkeypatch):
    expected = [
        FstabEntry(
            fs_spec='/dev/mapper/rhel_ibm--p8--kvm--03--guest--02-root',
            fs_file='/',
            fs_vfstype='xfs',
            fs_mntops='defaults',
            fs_freq='0',
            fs_passno='0'),
        FstabEntry(
            fs_spec='UUID=0a5215ef-1fb4-4b1b-8860-be4baa9e624c',
            fs_file='/boot',
            fs_vfstype='xfs',
            fs_mntops='defaults',
            fs_freq='0',
            fs_passno='1'),
        FstabEntry(
            fs_spec='UUID=acf9f525-3691-429f-96d7-3f8530227062',
            fs_file='/var',
            fs_vfstype='xfs',
            fs_mntops='defaults',
            fs_freq='0',
            fs_passno='0'),
        FstabEntry(
            fs_spec='UUID=d74186c9-21d5-4549-ae26-91ca9ed36f56',
            fs_file='/tmp',
            fs_vfstype='ext4',
            fs_mntops='defaults,nodev,nosuid,noexec',
            fs_freq='1',
            fs_passno='0'),
        FstabEntry(
            fs_spec='/dev/mapper/rhel_ibm--p8--kvm--03--guest--02-swap',
            fs_file='swap',
            fs_vfstype='swap',
            fs_mntops='defaults',
            fs_freq='0',
            fs_passno='0')]
    assert expected == storagescanner._get_fstab_info(os.path.join(CUR_DIR, 'files/fstab'))
    monkeypatch.setattr(storagescanner, '_is_file_readable', lambda dummy: False)
    assert [] == storagescanner._get_fstab_info('unreadable_file')


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

    storagescanner._get_fstab_info(os.path.join(CUR_DIR, 'files/invalid_fstab'))
    assert reporting.create_report.called == 1
    assert reporting.create_report.report_fields['severity'] == 'high'
    assert 'Problems with parsing data in /etc/fstab' in reporting.create_report.report_fields['title']
    assert is_inhibitor(reporting.create_report.report_fields)
    assert any("The fstab configuration file seems to be invalid" in msg for msg in api.current_logger.errmsg)


def test_get_mount_info(monkeypatch):
    expected = [
        MountEntry(
            name='sysfs', mount='/sys', tp='sysfs',
            options='rw,seclabel,nosuid,nodev,noexec,relatime'
        ),
        MountEntry(
            name='proc', mount='/proc', tp='proc',
            options='rw,nosuid,nodev,noexec,relatime'
        ),
        MountEntry(
            name='devtmpfs', mount='/dev', tp='devtmpfs',
            options='rw,seclabel,nosuid,size=16131092k,nr_inodes=4032773,mode=755'
        ),
        MountEntry(
            name='securityfs', mount='/sys/kernel/security', tp='securityfs',
            options='rw,nosuid,nodev,noexec,relatime'
        ),
        MountEntry(
            name='tmpfs', mount='/dev/shm', tp='tmpfs',
            options='rw,seclabel,nosuid,nodev'
        ),
        MountEntry(
            name='devpts', mount='/dev/pts', tp='devpts',
            options='rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000'
        ),
        MountEntry(
            name='tmpfs', mount='/run', tp='tmpfs',
            options='rw,seclabel,nosuid,nodev,mode=755'
        ),
        MountEntry(
            name='tmpfs', mount='/sys/fs/cgroup', tp='tmpfs',
            options='ro,seclabel,nosuid,nodev,noexec,mode=755'
        ),
        MountEntry(
            name='cgroup2', mount='/sys/fs/cgroup/unified', tp='cgroup2',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,nsdelegate'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/systemd', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,xattr,name=systemd'
        ),
        MountEntry(
            name='pstore', mount='/sys/fs/pstore', tp='pstore',
            options='rw,seclabel,nosuid,nodev,noexec,relatime'
        ),
        MountEntry(
            name='bpf', mount='/sys/fs/bpf', tp='bpf',
            options='rw,nosuid,nodev,noexec,relatime,mode=700'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/cpuset', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,cpuset'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/net_cls,net_prio', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,net_cls,net_prio'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/hugetlb', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,hugetlb'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/pids', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,pids'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/freezer', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,freezer'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/blkio', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,blkio'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/devices', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,devices'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/cpu,cpuacct', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,cpu,cpuacct'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/memory', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,memory'
        ),
        MountEntry(
            name='cgroup', mount='/sys/fs/cgroup/perf_event', tp='cgroup',
            options='rw,seclabel,nosuid,nodev,noexec,relatime,perf_event'
        ),
        MountEntry(
            name='configfs', mount='/sys/kernel/config', tp='configfs',
            options='rw,relatime'
        ),
        MountEntry(
            name='/dev/mapper/fedora-root', mount='/', tp='ext4',
            options='rw,seclabel,relatime'
        ),
        MountEntry(
            name='selinuxfs', mount='/sys/fs/selinux', tp='selinuxfs',
            options='rw,relatime'
        ),
        MountEntry(
            name='debugfs', mount='/sys/kernel/debug', tp='debugfs',
            options='rw,seclabel,relatime'
        ),
        MountEntry(
            name='hugetlbfs', mount='/dev/hugepages', tp='hugetlbfs',
            options='rw,seclabel,relatime,pagesize=2M'
        ),
        MountEntry(
            name='systemd-1', mount='/proc/sys/fs/binfmt_misc', tp='autofs',
            options='rw,relatime,fd=38,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=14019'
        ),
        MountEntry(
            name='mqueue', mount='/dev/mqueue', tp='mqueue',
            options='rw,seclabel,relatime'
        ),
        MountEntry(
            name='fusectl', mount='/sys/fs/fuse/connections', tp='fusectl',
            options='rw,relatime'
        ),
        MountEntry(
            name='tmpfs', mount='/tmp', tp='tmpfs',
            options='rw,seclabel,nosuid,nodev'
        ),
        MountEntry(
            name='/dev/nvme0n1p1', mount='/boot', tp='ext4',
            options='rw,seclabel,relatime'
        ),
        MountEntry(
            name='/dev/mapper/fedora-home', mount='/home', tp='ext4',
            options='rw,seclabel,relatime'
        ),
        MountEntry(
            name='sunrpc', mount='/var/lib/nfs/rpc_pipefs', tp='rpc_pipefs',
            options='rw,relatime'
        ),
        MountEntry(
            name='tmpfs', mount='/run/user/1000', tp='tmpfs',
            options='rw,seclabel,nosuid,nodev,relatime,size=3229704k,mode=700,uid=1000,gid=1000'
        ),
        MountEntry(
            name='tmpfs', mount='/run/user/42', tp='tmpfs',
            options='rw,seclabel,nosuid,nodev,relatime,size=3229704k,mode=700,uid=42,gid=42'
        ),
        MountEntry(
            name='gvfsd-fuse', mount='/run/user/1000/gvfs',
            tp='fuse.gvfsd-fuse', options='rw,nosuid,nodev,relatime,user_id=1000,group_id=1000'
        ),
        MountEntry(
            name='/dev/loop2p1', mount='/mnt/foo\\040bar', tp='iso9660',
            options='ro,nosuid,nodev,relatime,nojoliet,check=s,map=n,blocksize=2048'
        )
    ]

    assert expected == storagescanner._get_mount_info(os.path.join(CUR_DIR, 'files/mounts'))


def test_get_lsblk_info(monkeypatch):
    bytes_per_gb = 1 << 30

    def get_cmd_output_mocked(cmd, delim, expected_len):
        if cmd == ['lsblk', '-pbnr', '--output', 'NAME,MAJ:MIN,RM,SIZE,RO,TYPE,MOUNTPOINT']:
            output_lines_split_on_whitespace = [
                ['vda', '252:0', '0', str(40 * bytes_per_gb), '0', 'disk', ''],
                ['vda1', '252:1', '0', str(1 * bytes_per_gb), '0', 'part', '/boot'],
                ['vda2', '252:2', '0', str(39 * bytes_per_gb), '0', 'part', ''],
                ['rhel_ibm--p8--kvm--03--guest--02-root', '253:0', '0', str(38 * bytes_per_gb), '0', 'lvm', '/'],
                ['rhel_ibm--p8--kvm--03--guest--02-swap', '253:1', '0', str(1 * bytes_per_gb), '0', 'lvm', '[SWAP]']
            ]
            for output_line_parts in output_lines_split_on_whitespace:
                yield output_line_parts
        elif len(cmd) == 5 and cmd[:4] == ['lsblk', '-nr', '--output', 'NAME,KNAME,SIZE']:
            # We cannot have the output in a list, since the command is called per device. Therefore, we have to map
            # each device path to its output.
            output_lines_split_on_whitespace_per_device = {
                'vda': ['vda', 'vda', '40G'],
                'vda1': ['vda1', 'vda1', '1G'],
                'vda2': ['vda2', 'vda2', '39G'],
                'rhel_ibm--p8--kvm--03--guest--02-root': ['rhel_ibm--p8--kvm--03--guest--02-root', 'kname1', '38G'],
                'rhel_ibm--p8--kvm--03--guest--02-swap': ['rhel_ibm--p8--kvm--03--guest--02-swap', 'kname2', '1G']
            }
            dev_path = cmd[4]
            if dev_path not in output_lines_split_on_whitespace_per_device:
                raise ValueError('Attempting to call lsblk on an unexpected device: {}'.format(dev_path))
            yield output_lines_split_on_whitespace_per_device[dev_path]

        else:
            raise ValueError('Attempting to call unexpected command: {}'.format(cmd))

    monkeypatch.setattr(storagescanner, '_get_cmd_output', get_cmd_output_mocked)
    expected = [
        LsblkEntry(
            name='vda',
            kname='vda',
            maj_min='252:0',
            rm='0',
            size='40G',
            bsize=40 * bytes_per_gb,
            ro='0',
            tp='disk',
            mountpoint=''),
        LsblkEntry(
            name='vda1',
            kname='vda1',
            maj_min='252:1',
            rm='0',
            size='1G',
            bsize=1 * bytes_per_gb,
            ro='0',
            tp='part',
            mountpoint='/boot'),
        LsblkEntry(
            name='vda2',
            kname='vda2',
            maj_min='252:2',
            rm='0',
            size='39G',
            bsize=39 * bytes_per_gb,
            ro='0',
            tp='part',
            mountpoint=''),
        LsblkEntry(
            name='rhel_ibm--p8--kvm--03--guest--02-root',
            kname='kname1',
            maj_min='253:0',
            rm='0',
            size='38G',
            bsize=38 * bytes_per_gb,
            ro='0',
            tp='lvm',
            mountpoint='/'),
        LsblkEntry(
            name='rhel_ibm--p8--kvm--03--guest--02-swap',
            kname='kname2',
            maj_min='253:1',
            rm='0',
            size='1G',
            bsize=1 * bytes_per_gb,
            ro='0',
            tp='lvm',
            mountpoint='[SWAP]')]

    actual = storagescanner._get_lsblk_info()
    assert expected == actual


def test_get_pvs_info(monkeypatch):
    def get_cmd_output_mocked(cmd, delim, expected_len):
        return [
            ['/dev/vda2', 'rhel_ibm-p8-kvm-03-guest-02', 'lvm2', 'a--', '<39.00g', '4.00m']]

    monkeypatch.setattr(storagescanner, '_get_cmd_output', get_cmd_output_mocked)
    expected = [
        PvsEntry(
            pv='/dev/vda2',
            vg='rhel_ibm-p8-kvm-03-guest-02',
            fmt='lvm2',
            attr='a--',
            psize='<39.00g',
            pfree='4.00m')]
    assert expected == storagescanner._get_pvs_info()


def test_get_vgs_info(monkeypatch):
    def get_cmd_output_mocked(cmd, delim, expected_len):
        return [
            ['rhel_ibm-p8-kvm-03-guest-02', '1', '2', '0', 'wz--n-', '<39.00g', '4.00m']]

    monkeypatch.setattr(storagescanner, '_get_cmd_output', get_cmd_output_mocked)
    expected = [
        VgsEntry(
            vg='rhel_ibm-p8-kvm-03-guest-02',
            pv='1',
            lv='2',
            sn='0',
            attr='wz--n-',
            vsize='<39.00g',
            vfree='4.00m')]
    assert expected == storagescanner._get_vgs_info()


def test_get_lvdisplay_info(monkeypatch):
    def get_cmd_output_mocked(cmd, delim, expected_len):
        return [
            ['root', 'rhel_ibm-p8-kvm-03-guest-02', '-wi-ao----', '37.99g', '', '', '', '', '', '', '', ''],
            ['swap', 'rhel_ibm-p8-kvm-03-guest-02', '-wi-ao----', '1.00g', '', '', '', '', '', '', '', '']]

    monkeypatch.setattr(storagescanner, '_get_cmd_output', get_cmd_output_mocked)
    expected = [
        LvdisplayEntry(
            lv='root',
            vg='rhel_ibm-p8-kvm-03-guest-02',
            attr='-wi-ao----',
            lsize='37.99g',
            pool='',
            origin='',
            data='',
            meta='',
            move='',
            log='',
            cpy_sync='',
            convert=''),
        LvdisplayEntry(
            lv='swap',
            vg='rhel_ibm-p8-kvm-03-guest-02',
            attr='-wi-ao----',
            lsize='1.00g',
            pool='',
            origin='',
            data='',
            meta='',
            move='',
            log='',
            cpy_sync='',
            convert='')]
    assert expected == storagescanner._get_lvdisplay_info()


def test_get_systemd_mount_info(monkeypatch):

    class UdevDeviceMocked(object):
        def __init__(self, device_node, path, model, wwn, fs_type, label, uuid):
            self.device_node = device_node
            # Simulate udev device attributes that should be queried
            self.device_attributes = {
                'ID_PATH': path,
                'ID_MODEL': model,
                'ID_WWN': wwn,
                'ID_FS_TYPE': fs_type,
                'ID_FS_LABEL': label,
                'ID_FS_UUID': uuid,
            }

        def get(self, attribute, default=None):
            if attribute not in self.device_attributes:
                raise KeyError('Actor tried to query an udev device attribute that is not a part of the mocks.')

            if self.device_attributes[attribute] is None:
                return default

            return self.device_attributes[attribute]

    class UdevContextMocked(object):
        def __init__(self, mocked_devices):
            self.mocked_devices = mocked_devices

        def list_devices(self, **dummy_kwargs):
            return self.mocked_devices

    mocked_block_devices = [
        UdevDeviceMocked(
            device_node='/dev/dm-1',
            path=None,
            model=None,
            wwn=None,
            fs_type='ext4',
            label=None,
            uuid='bec30ca5-5403-4c23-ae6e-cb2a911bc076'),
        UdevDeviceMocked(
            device_node='/dev/dm-3',
            path=None,
            model=None,
            wwn=None,
            fs_type='ext4',
            label=None,
            uuid='d6eaf17d-e2a9-4e8d-bb54-a89c18923ea2'),
        UdevDeviceMocked(
            device_node='/dev/sda1',
            path='pci-0000:00:17.0-ata-2',
            model='LITEON_LCH-256V2S',
            wwn='0x5002303100d82b06',
            fs_type='ext4',
            label=None,
            uuid='c3890bf3-9273-4877-ad1f-68144e1eb858')]

    # Partially apply mocked_block_devices to the UdevContextMocked, so that it
    # is OK to initialize it with no arguments (same as original Context)
    monkeypatch.setattr(pyudev, 'Context', functools.partial(UdevContextMocked, mocked_block_devices))
    expected = [
        SystemdMountEntry(
            node='/dev/dm-1',
            path='n/a',
            model='n/a',
            wwn='n/a',
            fs_type='ext4',
            label='n/a',
            uuid='bec30ca5-5403-4c23-ae6e-cb2a911bc076'),
        SystemdMountEntry(
            node='/dev/dm-3',
            path='n/a',
            model='n/a',
            wwn='n/a',
            fs_type='ext4',
            label='n/a',
            uuid='d6eaf17d-e2a9-4e8d-bb54-a89c18923ea2'),
        SystemdMountEntry(
            node='/dev/sda1',
            path='pci-0000:00:17.0-ata-2',
            model='LITEON_LCH-256V2S',
            wwn='0x5002303100d82b06',
            fs_type='ext4',
            label='n/a',
            uuid='c3890bf3-9273-4877-ad1f-68144e1eb858')]
    assert expected == storagescanner._get_systemd_mount_info()


# repos/system_upgrade/common/actors/systemd/checksystemdbrokensymlinks/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checksystemdbrokensymlinks
from leapp.models import SystemdBrokenSymlinksSource, SystemdServicesInfoSource
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckSystemdBrokenSymlinks(Actor):
    """
    Check whether some systemd symlinks are broken

    If some systemd symlinks are broken, report them but do not inhibit the
    upgrade. The symlinks broken already before the upgrade will not be
    handled by the upgrade process anyhow. Two different reports are created:
    - symlinks which have the same filename as an existing enabled systemd
      service (the symlink doesn't point to an existing unit file, but the
      service is enabled)
    - broken symlinks which names do not correspond with any existing systemd
      unit file (typically when the service is removed but not disabled
      correctly)
    """

    name = 'check_systemd_broken_symlinks'
    consumes = (SystemdBrokenSymlinksSource, SystemdServicesInfoSource)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checksystemdbrokensymlinks.process()


# repos/system_upgrade/common/actors/systemd/checksystemdservicetasks/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checksystemdservicetasks
from leapp.models import SystemdServicesTasks
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag


class CheckSystemdServicesTasks(Actor):
    """
    Inhibit the upgrade if SystemdServicesTasks tasks are in conflict

    SystemdServicesTasks messages with conflicting requested service states
    could be produced. For example a service could be requested to be both
    - enabled and disabled. This actor inhibits upgrade in such cases.

    Note: We expect that SystemdServicesTasks could be produced even after the
    TargetTransactionChecksPhase (e.g. during the ApplicationPhase). The
    purpose of this actor is to report collisions in case we can already detect
    them. In case of conflicts caused by messages produced later we just log
    the collisions and the services will end up disabled.
    """

    name = 'check_systemd_services_tasks'
    consumes = (SystemdServicesTasks,)
    produces = (Report,)
    tags = (TargetTransactionChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checksystemdservicetasks.check_conflicts()


# repos/system_upgrade/common/actors/systemd/repairsystemdsymlinks/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import repairsystemdsymlinks
from leapp.models import SystemdBrokenSymlinksSource, SystemdBrokenSymlinksTarget, SystemdServicesInfoSource
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class RepairSystemdSymlinks(Actor):
    """
    Fix broken or incorrect systemd symlinks

    Symlinks are handled in the following fashion, if the symlink points to:
        - a removed unit, such a symlink is deleted
        - a unit whose installation has been changed (e.g. changed WantedBy),
          such symlinks are fixed (re-enabled using systemctl)

    Symlinks that have been already broken before the in-place upgrade are ignored.
    """

    name = 'repair_systemd_symlinks'
    consumes = (SystemdBrokenSymlinksSource, SystemdBrokenSymlinksTarget, SystemdServicesInfoSource)
    produces = ()
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)

    def process(self):
        repairsystemdsymlinks.process()


# repos/system_upgrade/common/actors/systemd/scansystemdsource/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scansystemdsource
from leapp.models import SystemdBrokenSymlinksSource, SystemdServicesInfoSource, SystemdServicesPresetInfoSource
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanSystemdSource(Actor):
    """
    Provides info about systemd on the source system

    The provided info includes information about:
    - vendor presets of services
    - systemd service files, including their state
    - broken systemd symlinks

    There is an analogous actor :class:`ScanSystemdTarget` for target system.
    """

    name = 'scan_systemd_source'
    consumes = ()
    produces = (SystemdBrokenSymlinksSource, SystemdServicesInfoSource, SystemdServicesPresetInfoSource)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scansystemdsource.scan()


# repos/system_upgrade/common/actors/systemd/scansystemdtarget/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scansystemdtarget
from leapp.models import SystemdBrokenSymlinksTarget, SystemdServicesInfoTarget, SystemdServicesPresetInfoTarget
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class ScanSystemdTarget(Actor):
    """
    Provides info about systemd on the source system

    The provided info includes information about:
    - vendor presets of services
    - systemd service files, including their state
    - broken systemd symlinks

    There is an analogous actor :class:`ScanSystemdSource` for source system

    The actor ignore errors (errors are logged, but do not stop the upgrade).
    If some data cannot be obtained, particular message is not produced.
    Actors are expected to check whether the data is available.
    """
    name = 'scan_systemd_target'
    consumes = ()
    produces = (SystemdBrokenSymlinksTarget, SystemdServicesInfoTarget, SystemdServicesPresetInfoTarget)
    tags = (IPUWorkflowTag, ApplicationsPhaseTag)

    def process(self):
        scansystemdtarget.scan()


# repos/system_upgrade/common/actors/systemd/setsystemdservicesstates/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import setsystemdservicesstate
from leapp.models import SystemdServicesTasks
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class SetSystemdServicesState(Actor):
    """
    According to input messages sets systemd services states on the target system
    """

    name = 'set_systemd_services_state'
    consumes = (SystemdServicesTasks,)
    produces = ()
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        setsystemdservicesstate.process()


# repos/system_upgrade/common/actors/systemfacts/libraries/systemfacts.py

import errno
import functools
import grp
import logging
import os
import pwd
import re

import six

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import repofileutils
from leapp.libraries.common.config import architecture
from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import (
    ActiveKernelModule,
    ActiveKernelModulesFacts,
    DefaultGrub,
    DefaultGrubInfo,
    FirewallsFacts,
    FirewallStatus,
    FirmwareFacts,
    Group,
    GroupsFacts,
    GrubCfgBios,
    KernelModuleParameter,
    RepositoriesFacts,
    SELinuxFacts,
    SysctlVariable,
    SysctlVariablesFacts,
    User,
    UsersFacts
)


def aslist(f):
    """ Decorator used to convert generator to list """
    @functools.wraps(f)
    def inner(*args, **kwargs):
        return list(f(*args, **kwargs))
    return inner


def anyendswith(value, ends):
    """ Check if `value` ends with one of the possible `ends` """
    for end in ends:
        if value.endswith(end):
            return True
    return False


def anyhasprefix(value, prefixes):
    """ Check if `value` starts with on of the possible `prefixes` """
    for p in prefixes:
        if value.startswith(p):
            return True
    return False


@aslist
def _get_system_users():
    for p in pwd.getpwall():
        yield User(
            name=p.pw_name,
            uid=p.pw_uid,
            gid=p.pw_gid,
            home=p.pw_dir
        )


def get_system_users_status():
    """ Get a list of users from `/etc/passwd` """
    return UsersFacts(users=_get_system_users())


@aslist
def _get_system_groups():
    for g in grp.getgrall():
        yield Group(
            name=g.gr_name,
            gid=g.gr_gid,
            members=g.gr_mem
        )


def get_system_groups_status():
    """ Get a list of groups from `/etc/groups` """
    return GroupsFacts(groups=_get_system_groups())


@aslist
def _get_active_kernel_modules(logger):
    lines = run(['lsmod'], split=True)['stdout']
    for l in lines[1:]:
        name = l.split(' ')[0]

        # Read parameters of the given module as exposed by the
        # `/sys` VFS, if there are no parameters exposed we just
        # take the name of the module
        base_path = '/sys/module/{module}'.format(module=name)
        parameters_path = os.path.join(base_path, 'parameters')
        if not os.path.exists(parameters_path):
            yield ActiveKernelModule(filename=name, parameters=[])
            continue

        # Use `modinfo` to probe for signature information
        parameter_dict = {}
        try:
            signature = run(['modinfo', '-F', 'signature', name], split=False)['stdout']
        except CalledProcessError:
            signature = None

        signature_string = None
        if signature:
            # Remove whitespace from the signature string
            signature_string = re.sub(r"\s+", "", signature, flags=re.UNICODE)

        # Since we're using the `/sys` VFS we need to use `os.listdir()` to get
        # all the property names and then just read from all the listed paths
        parameters = sorted(os.listdir(parameters_path))
        for param in parameters:
            try:
                with open(os.path.join(parameters_path, param), mode='r') as fp:
                    parameter_dict[param] = fp.read().strip()
            except IOError as exc:
                # Some parameters are write-only, in that case we just log the name of parameter
                # and the module and continue
                if exc.errno in (errno.EACCES, errno.EPERM):
                    msg = 'Unable to read parameter "{param}" of kernel module "{name}"'
                    logger.warning(msg.format(param=param, name=name))
                else:
                    raise exc

        # Project the dictionary as a list of key values
        items = [
            KernelModuleParameter(name=k, value=v)
            for (k, v) in six.iteritems(parameter_dict)
        ]

        yield ActiveKernelModule(
            filename=name,
            parameters=items,
            signature=signature_string
        )


def get_active_kernel_modules_status(logger):
    """ Get a list of active kernel modules """
    return ActiveKernelModulesFacts(kernel_modules=_get_active_kernel_modules(logger))


@aslist
def _get_sysctls():
    unstable = ('fs.dentry-state', 'fs.file-nr', 'fs.inode-nr',
                'fs.inode-state', 'kernel.random.uuid', 'kernel.random.entropy_avail',
                'kernel.ns_last_pid', 'net.netfilter.nf_conntrack_count',
                'net.netfilter.nf_conntrack_events', 'kernel.sched_domain.',
                'dev.cdrom.info', 'kernel.pty.nr')

    variables = []
    for sc in run(['sysctl', '-a'], split=True)['stdout']:
        name = sc.split(' ', 1)[0]
        # if the sysctl name has an unstable prefix, we skip
        if anyhasprefix(name, unstable):
            continue
        variables.append(sc)

    # sort our variables so they can be diffed directly when needed
    for var in sorted(variables):
        name, value = tuple(map(type(var).strip, var.split('=', 1)))
        yield SysctlVariable(
            name=name,
            value=value
        )


def get_sysctls_status():
    r""" Get a list of stable `sysctls` variables

        Note that some variables are inherently unstable and we need to blacklist
        them:

        diff -u <(sysctl -a 2>/dev/null | sort) <(sysctl -a 2>/dev/null | sort)\
                | grep -E '^\+[a-z]'\
                | cut -d' ' -f1\
                | cut -d+ -f2
    """
    return SysctlVariablesFacts(sysctl_variables=_get_sysctls())


def get_repositories_status():
    """ Get a basic information about YUM repositories installed in the system """
    return RepositoriesFacts(repositories=repofileutils.get_parsed_repofiles())


def get_selinux_status():
    """ Get SELinux status information """
    # will be None if something went wrong or contain SELinuxFacts otherwise
    res = None
    try:
        import selinux  # pylint: disable=import-outside-toplevel
    except ImportError:
        api.report_error("SELinux Import Error", details="libselinux-python package must be installed.")
        return res

    outdata = dict({'enabled': selinux.is_selinux_enabled() == 1})
    outdata['mls_enabled'] = selinux.is_selinux_mls_enabled() == 1

    try:
        outdata['runtime_mode'] = "enforcing" if selinux.security_getenforce() == 1 else "permissive"
        # FIXME: check selinux_getenforcemode[0] (that should be return value of a underneath function)
        enforce_mode = selinux.selinux_getenforcemode()[1]
        if enforce_mode >= 0:
            outdata['static_mode'] = "enforcing" if enforce_mode == 1 else "permissive"
        else:
            outdata['static_mode'] = "disabled"
        outdata['policy'] = selinux.selinux_getpolicytype()[1]
    except OSError:
        # This happens when SELinux is disabled
        # [Errno 2] No such file or directory
        outdata['runtime_mode'] = 'permissive'
        outdata['static_mode'] = 'disabled'
        outdata['policy'] = 'targeted'

    res = SELinuxFacts(**outdata)
    return res


def get_firewalls_status():
    """ Get firewalld status information """
    logger = logging.getLogger('get_firewalld_status')

    def _get_firewall_status(service_name):
        try:
            ret_list = run(['systemctl', 'is-active', service_name], split=True)['stdout']
            active = ret_list[0] == 'active'
        except CalledProcessError:
            active = False
            logger.debug('The %s service is likely not active', service_name)

        try:
            ret_list = run(['systemctl', 'is-enabled', service_name], split=True)['stdout']
            enabled = ret_list[0] == 'enabled'
        except CalledProcessError:
            enabled = False
            logger.debug('The %s service is likely not enabled nor running', service_name)

        return FirewallStatus(
            active=active,
            enabled=enabled,
        )

    return FirewallsFacts(
        firewalld=_get_firewall_status('firewalld'),
        iptables=_get_firewall_status('iptables'),
        ip6tables=_get_firewall_status('ip6tables'),
    )


def get_firmware():
    firmware = 'efi' if os.path.isdir('/sys/firmware/efi') else 'bios'
    if architecture.matches_architecture(architecture.ARCH_PPC64LE):
        ppc64le_opal = bool(os.path.isdir('/sys/firmware/opal/'))
        return FirmwareFacts(firmware=firmware, ppc64le_opal=ppc64le_opal)
    return FirmwareFacts(firmware=firmware)


@aslist
def _default_grub_info():
    default_grb_fpath = '/etc/default/grub'
    if not os.path.isfile(default_grb_fpath):
        reporting.create_report([
            reporting.Title('File "{}" does not exist!'.format(default_grb_fpath)),
            reporting.Summary(
                'Leapp detected "{}" does not exist. The file is essential for the in-place upgrade '
                'to finish successfully. This scenario might have occurred if the system was already '
                'upgraded from RHEL 6. Please re-create the file manually.'.format(default_grb_fpath)
            ),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Groups([reporting.Groups.BOOT]),
            reporting.RelatedResource('file', default_grb_fpath),
            reporting.ExternalLink(
                url='https://access.redhat.com/solutions/3185891',
                title='How to re-create the missing "{}" file in Red Hat Enterprise Linux 7?'.format(
                    default_grb_fpath
                )
            ),
        ])
    else:
        for line in run(['cat', default_grb_fpath], split=True)['stdout']:
            line = line.strip()
            if not line or line[0] == '#':
                # skip comments and empty lines
                continue
            try:
                name, value = tuple(map(type(line).strip, line.split('=', 1)))
            except ValueError as e:
                # we do not want to really continue when we cannot parse this file
                # TODO(pstodulk): rewrite this in the form we produce inhibitor
                # with problematic lines. This is improvement just in comparison
                # to the original hard crash.
                raise StopActorExecutionError(
                    'Failed parsing of {}'.format(default_grb_fpath),
                    details={
                        'error': str(e),
                        'problematic line': str(line)
                    }
                )

            yield DefaultGrub(
                name=name,
                value=value
            )


def get_default_grub_conf():
    """ Get a list of GRUB parameters from /etc/default/grub """
    return DefaultGrubInfo(default_grub_info=_default_grub_info())


def get_bios_grubcfg_details():
    """ Get BIOS (non-EFI) Grub config details """
    if get_firmware().firmware == 'bios' and not architecture.matches_architecture(architecture.ARCH_S390X):
        with open('/boot/grub2/grub.cfg') as fo:
            content = fo.read()
        insmod_bls = bool('insmod blscfg' in content)
        return GrubCfgBios(insmod_bls=insmod_bls)
    return None


# repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts.py

from leapp.libraries.actor.systemfacts import anyendswith, anyhasprefix, aslist
from leapp.snactor.fixture import current_actor_libraries


def test_anyendswith(current_actor_libraries):
    value = 'this_is_a_test'

    assert anyendswith(value, ['a_test'])
    assert anyendswith(value, ['a_test', 'bgerwh', 'g52h4q'])
    assert anyendswith(value, ['est'])
    assert not anyendswith(value, ['asafsaf', 'gbfdshh', '123f', 'gdsgsnb'])
    assert not anyendswith(value, [])


def test_anyhasprefix(current_actor_libraries):
    value = 'this_is_a_test'

    assert anyhasprefix(value, ['this'])
    assert anyhasprefix(value, ['this', 'ddsvssd', 'bsdhn', '125fff'])
    assert anyhasprefix(value, ['this_is'])
    assert not anyhasprefix(value, ['ccbbb', 'xasbnn', 'xavavav', 'bbnkk1'])
    assert not anyhasprefix(value, [])


def test_aslist(current_actor_libraries):

    @aslist
    def local():
        yield True
        yield False
        yield True

    r = local()

    assert isinstance(r, list) and r[0] and r[2] and not r[1]


# repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_grub.py

import os

from leapp.libraries.actor import systemfacts
from leapp.models import DefaultGrub


class RunMocked(object):
    def __init__(self, cmd_result):
        self.called = 0
        self.cmd_result = cmd_result
        self.split = False
        self.cmd = None

    def __call__(self, cmd, split=False):
        self.cmd = cmd
        self.split = split
        self.called += 1
        return self.cmd_result


def test_default_grub_info_valid(monkeypatch):
    mocked_run = RunMocked({
        'stdout': [
            'line="whatever else here"',
            'newline="whatever"',
            '# comment here',
            'why_not=value',
            ' # whitespaces around comment ',
            ' ',
            ' last=last really'
        ],
    })
    expected_result = [
        DefaultGrub(name='line', value='"whatever else here"'),
        DefaultGrub(name='newline', value='"whatever"'),
        DefaultGrub(name='why_not', value='value'),
        DefaultGrub(name='last', value='last really'),
    ]
    monkeypatch.setattr(systemfacts, 'run', mocked_run)
    monkeypatch.setattr(os.path, 'isfile', lambda dummy: True)
    for msg in systemfacts._default_grub_info():
        expected_msg = expected_result.pop(0)
        assert msg.name == expected_msg.name
        assert msg.value == expected_msg.value
    assert mocked_run.called
    assert not expected_result


# repos/system_upgrade/common/actors/systemfacts/tests/test_systemfacts_selinux.py

import warnings

import pytest

from leapp.libraries.actor.systemfacts import get_selinux_status
from leapp.models import SELinuxFacts

no_selinux = False
try:
    import selinux
except ImportError:
    no_selinux = True
    warnings.warn(
        'Tests which uses `selinux` will be skipped'
        ' due to library unavailability.', ImportWarning)


reason_to_skip_msg = "Selinux is not available"

# FIXME: create valid tests...


@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
def test_selinux_enabled_enforcing(monkeypatch):
    """
    Test case SELinux is enabled in enforcing mode
    """
    monkeypatch.setattr(selinux, 'is_selinux_mls_enabled', lambda: 1)
    monkeypatch.setattr(selinux, 'security_getenforce', lambda: 1)
    monkeypatch.setattr(selinux, 'selinux_getenforcemode', lambda: [0, 1])
    monkeypatch.setattr(selinux, 'is_selinux_enabled', lambda: 1)
    monkeypatch.setattr(selinux, 'selinux_getpolicytype', lambda: [0, 'targeted'])
    expected_data = {'policy': 'targeted',
                     'mls_enabled': True,
                     'enabled': True,
                     'runtime_mode': 'enforcing',
                     'static_mode': 'enforcing'}
    assert SELinuxFacts(**expected_data) == get_selinux_status()


@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
def test_selinux_enabled_permissive(monkeypatch):
    """
    Test case SELinux is enabled in permissive mode
    """
    monkeypatch.setattr(selinux, 'is_selinux_mls_enabled', lambda: 1)
    monkeypatch.setattr(selinux, 'security_getenforce', lambda: 0)
    monkeypatch.setattr(selinux, 'selinux_getenforcemode', lambda: [0, 0])
    monkeypatch.setattr(selinux, 'is_selinux_enabled', lambda: 1)
    monkeypatch.setattr(selinux, 'selinux_getpolicytype', lambda: [0, 'targeted'])
    expected_data = {'policy': 'targeted',
                     'mls_enabled': True,
                     'enabled': True,
                     'runtime_mode': 'permissive',
                     'static_mode': 'permissive'}
    assert SELinuxFacts(**expected_data) == get_selinux_status()


@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
def test_selinux_disabled(monkeypatch):
    """
    Test case SELinux is disabled
    """
    monkeypatch.setattr(selinux, 'is_selinux_mls_enabled', lambda: 0)
    monkeypatch.setattr(selinux, 'security_getenforce', lambda: 0)
    monkeypatch.setattr(selinux, 'selinux_getenforcemode', lambda: [0, 0])
    monkeypatch.setattr(selinux, 'is_selinux_enabled', lambda: 0)
    monkeypatch.setattr(selinux, 'selinux_getpolicytype', lambda: [0, 'targeted'])
    expected_data = {'policy': 'targeted',
                     'mls_enabled': False,
                     'enabled': False,
                     'runtime_mode': 'permissive',
                     'static_mode': 'permissive'}
    assert SELinuxFacts(**expected_data) == get_selinux_status()


class MockNoConfigFileOSError(object):
    def __init__(self):
        raise OSError


@pytest.mark.skipif(no_selinux, reason=reason_to_skip_msg)
def test_selinux_disabled_no_config_file(monkeypatch):
    """
    Test case SELinux is disabled
    """
    monkeypatch.setattr(selinux, 'is_selinux_mls_enabled', lambda: 0)
    monkeypatch.setattr(selinux, 'security_getenforce', lambda: 0)
    monkeypatch.setattr(selinux, 'selinux_getenforcemode', MockNoConfigFileOSError)
    monkeypatch.setattr(selinux, 'is_selinux_enabled', lambda: 0)
    monkeypatch.setattr(selinux, 'selinux_getpolicytype', lambda: [0, 'targeted'])
    expected_data = {'policy': 'targeted',
                     'mls_enabled': False,
                     'enabled': False,
                     'runtime_mode': 'permissive',
                     'static_mode': 'disabled'}

    assert SELinuxFacts(**expected_data) == get_selinux_status()


# repos/system_upgrade/common/actors/targetuserspacecreator/libraries/constants.py

import os

SCRATCH_DIR = os.getenv('LEAPP_CONTAINER_ROOT', '/var/lib/leapp/scratch')
MOUNTS_DIR = os.path.join(SCRATCH_DIR, 'mounts')
TARGET_USERSPACE = '/var/lib/leapp/el{}userspace'


# repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py

import itertools
import os

from leapp import reporting
from leapp.exceptions import StopActorExecution, StopActorExecutionError
from leapp.libraries.actor import constants
from leapp.libraries.common import dnfplugin, mounting, overlaygen, repofileutils, rhsm, rhui, utils
from leapp.libraries.common.config import get_env, get_product_type
from leapp.libraries.common.config.version import get_target_major_version
from leapp.libraries.stdlib import api, CalledProcessError, config, run
from leapp.models import RequiredTargetUserspacePackages  # deprecated
from leapp.models import TMPTargetRepositoriesFacts  # deprecated all the time
from leapp.models import (
    CustomTargetRepositoryFile,
    PkgManagerInfo,
    RepositoriesFacts,
    RHSMInfo,
    RHUIInfo,
    StorageInfo,
    TargetOSInstallationImage,
    TargetRepositories,
    TargetUserSpaceInfo,
    TargetUserSpacePreupgradeTasks,
    UsedTargetRepositories,
    UsedTargetRepository,
    XFSPresence
)
from leapp.utils.deprecation import suppress_deprecation

# TODO: "refactor" (modify) the library significantly
# The current shape is really bad and ineffective (duplicit parsing
# of repofiles). The library is doing 3 (5) things:
# # (0.) consume process input data
# # 1. prepare the first container, to be able to obtain repositories for the
# #    target system (this is extra neededwhen rhsm is used, but not reason to
# #    do such thing only when rhsm is used. Be persistent here
# # 2. gather target repositories that should AND can be used
# #    - basically here is the main thing that is PITA; I started
# #      the refactoring but realized that it needs much more changes because
# #      of RHSM...
# # 3. create the target userspace bootstrap
# # (4.) produce messages with the data
#
# Because of the lack of time, I am extending the current bad situation,
# but after the release, the related code should be really refactored.
# It would be probably ideal, if this and other actors in the current and the
# next phase are modified properly and we could create inhibitors in the check
# phase and keep everything on the report. But currently it seems it doesn't
# worth to invest so much energy into it. So let's just make this really
# readable (includes split of the functionality into several libraries)
# and do not mess.
# Issue: #486

PROD_CERTS_FOLDER = 'prod-certs'
PERSISTENT_PACKAGE_CACHE_DIR = '/var/lib/leapp/persistent_package_cache'


def _check_deprecated_rhsm_skip():
    # we do not plan to cover this case by tests as it is purely
    # devel/testing stuff, that becomes deprecated now
    # just log the warning now (better than nothing?); deprecation process will
    # be specified in close future
    if get_env('LEAPP_DEVEL_SKIP_RHSM', '0') == '1':
        api.current_logger().warning(
            'The LEAPP_DEVEL_SKIP_RHSM has been deprecated. Use'
            ' LEAPP_NO_RHSM instead or use the --no-rhsm option for'
            ' leapp. as well custom repofile has not been defined.'
            ' Please read documentation about new "skip rhsm" solution.'
        )


class _InputData(object):
    def __init__(self):
        self._consume_data()

    @suppress_deprecation(RequiredTargetUserspacePackages)
    def _consume_data(self):
        """
        Wrapper function to consume majority input data.

        It doesn't consume TargetRepositories, which are consumed in the
        own function.
        """
        self.packages = {'dnf', 'dnf-command(config-manager)'}
        self.files = []
        _cftuples = set()

        def _update_files(copy_files):
            # add just uniq CopyFile objects to omit duplicate copying of files
            for cfile in copy_files:
                cftuple = (cfile.src, cfile.dst)
                if cftuple not in _cftuples:
                    _cftuples.add(cftuple)
                    self.files.append(cfile)

        for task in api.consume(TargetUserSpacePreupgradeTasks):
            self.packages.update(task.install_rpms)
            _update_files(task.copy_files)

        for message in api.consume(RequiredTargetUserspacePackages):
            self.packages.update(message.packages)

        # Get the RHSM information (available repos, attached SKUs, etc.) of the source system
        self.rhsm_info = next(api.consume(RHSMInfo), None)
        self.rhui_info = next(api.consume(RHUIInfo), None)
        if not self.rhsm_info and not rhsm.skip_rhsm():
            api.current_logger().warning('Could not receive RHSM information - Is this system registered?')
            raise StopActorExecution()
        if rhsm.skip_rhsm() and self.rhsm_info:
            # this should not happen. if so, raise an error as something in
            # other actors is wrong really
            raise StopActorExecutionError("RHSM is not handled but the RHSMInfo message has been produced.")

        self.custom_repofiles = list(api.consume(CustomTargetRepositoryFile))
        self.xfs_info = next(api.consume(XFSPresence), XFSPresence())
        self.storage_info = next(api.consume(StorageInfo), None)
        if not self.storage_info:
            raise StopActorExecutionError('No storage info available cannot proceed.')


def _restore_persistent_package_cache(userspace_dir):
    if get_env('LEAPP_DEVEL_USE_PERSISTENT_PACKAGE_CACHE', None) == '1':
        if os.path.exists(PERSISTENT_PACKAGE_CACHE_DIR):
            with mounting.NspawnActions(base_dir=userspace_dir) as target_context:
                target_context.copytree_to(PERSISTENT_PACKAGE_CACHE_DIR, '/var/cache/dnf')
    # We always want to remove the persistent cache here to unclutter the system
    run(['rm', '-rf', PERSISTENT_PACKAGE_CACHE_DIR])


def _backup_to_persistent_package_cache(userspace_dir):
    if get_env('LEAPP_DEVEL_USE_PERSISTENT_PACKAGE_CACHE', None) == '1':
        # Clean up any dead bodies, just in case
        run(['rm', '-rf', PERSISTENT_PACKAGE_CACHE_DIR])
        if os.path.exists(os.path.join(userspace_dir, 'var', 'cache', 'dnf')):
            with mounting.NspawnActions(base_dir=userspace_dir) as target_context:
                target_context.copytree_from('/var/cache/dnf', PERSISTENT_PACKAGE_CACHE_DIR)


def prepare_target_userspace(context, userspace_dir, enabled_repos, packages):
    """
    Implement the creation of the target userspace.
    """
    _backup_to_persistent_package_cache(userspace_dir)

    target_major_version = get_target_major_version()
    run(['rm', '-rf', userspace_dir])
    _create_target_userspace_directories(userspace_dir)
    with mounting.BindMount(
        source=userspace_dir, target=os.path.join(context.base_dir, 'el{}target'.format(target_major_version))
    ):
        _restore_persistent_package_cache(userspace_dir)

        repos_opt = [['--enablerepo', repo] for repo in enabled_repos]
        repos_opt = list(itertools.chain(*repos_opt))
        cmd = ['dnf',
               'install',
               '-y',
               '--nogpgcheck',
               '--setopt=module_platform_id=platform:el{}'.format(target_major_version),
               '--setopt=keepcache=1',
               '--releasever', api.current_actor().configuration.version.target,
               '--installroot', '/el{}target'.format(target_major_version),
               '--disablerepo', '*'
               ] + repos_opt + packages
        if config.is_verbose():
            cmd.append('-v')
        if rhsm.skip_rhsm():
            cmd += ['--disableplugin', 'subscription-manager']
        try:
            context.call(cmd, callback_raw=utils.logging_handler)
        except CalledProcessError as exc:
            message = 'Unable to install RHEL {} userspace packages.'.format(target_major_version)
            details = {'details': str(exc), 'stderr': exc.stderr}

            # If a proxy was set in dnf config, it should be the reason why dnf
            # failed since leapp does not support updates behind proxy yet.
            for manager_info in api.consume(PkgManagerInfo):
                if manager_info.configured_proxies:
                    details['details'] = ("DNF failed to install userspace packages, likely due to the proxy "
                                          "configuration detected in the YUM/DNF configuration file.")

            # Similarly if a proxy was set specifically for one of the repositories.
            for repo_facts in api.consume(RepositoriesFacts):
                for repo_file in repo_facts.repositories:
                    if any(repo_data.proxy and repo_data.enabled for repo_data in repo_file.data):
                        details['details'] = ("DNF failed to install userspace packages, likely due to the proxy "
                                              "configuration detected in a repository configuration file.")

            raise StopActorExecutionError(message=message, details=details)


def _get_all_rhui_pkgs():
    """
    Return the list of rhui packages

    Currently, do not care about what rhui we have, release, etc.
    Just take all packages. We need them just for the purpose of filtering
    what files we have to remove (see _prep_repository_access) and it's ok
    for us to use whatever rhui rpms (the relevant rpms catch the problem,
    the others are just taking bytes in memory...). It's a hot-fix. We are going
    to refactor the library later completely..
    """
    upg_path = rhui.get_upg_path()
    pkgs = []
    for rhui_map in rhui.RHUI_CLOUD_MAP[upg_path].values():
        for key in rhui_map.keys():
            if not key.endswith('pkg'):
                continue
            pkgs.append(rhui_map[key])
    return pkgs


def _get_files_owned_by_rpms(context, dirpath, pkgs=None):
    """
    Return the list of file names inside dirpath owned by RPMs.

    This is important e.g. in case of RHUI which installs specific repo files
    in the yum.repos.d directory.

    In case the pkgs param is None or empty, do not filter any specific rpms.
    Otherwise return filenames that are owned by any pkg in the given list.
    """
    files_owned_by_rpms = []
    for fname in os.listdir(context.full_path(dirpath)):
        try:
            result = context.call(['rpm', '-qf', os.path.join(dirpath, fname)])
        except CalledProcessError:
            api.current_logger().debug('SKIP the {} file: not owned by any rpm'.format(fname))
            continue
        if pkgs and not [pkg for pkg in pkgs if pkg in result['stdout']]:
            api.current_logger().debug('SKIP the {} file: not owned by any searched rpm:'.format(fname))
            continue
        api.current_logger().debug('Found the file owned by an rpm: {}.'.format(fname))
        files_owned_by_rpms.append(fname)
    return files_owned_by_rpms


def _copy_certificates(context, target_userspace):
    """
    Copy the needed cetificates into the container, but preserve original ones

    Some certificates are already installed in the container and those are
    default certificates for the target OS. We know we should preserve at
    least certificates located at rpm-gpg directory. So preserve these for
    now at least.
    """
    target_pki = os.path.join(target_userspace, 'etc', 'pki')
    backup_pki = os.path.join(target_userspace, 'etc', 'pki.backup')

    # FIXME(pstodulk): search for all files owned by RPMs inside the container
    # before the mv, and all such files restore
    # - this is requirement to not break IPU with RHUI when making the copy
    # of certificates unconditional
    run(['mv', target_pki, backup_pki])
    context.copytree_from('/etc/pki', target_pki)

    # TODO(pstodulk): restore the files owned by rpms instead of the code below
    for fname in os.listdir(os.path.join(backup_pki, 'rpm-gpg')):
        src_path = os.path.join(backup_pki, 'rpm-gpg', fname)
        dst_path = os.path.join(target_pki, 'rpm-gpg', fname)
        run(['rm', '-rf', dst_path])
        run(['cp', '-a', src_path, dst_path])


def _prep_repository_access(context, target_userspace):
    """
    Prepare repository access by copying all relevant certificates and configuration files to the userspace
    """
    target_etc = os.path.join(target_userspace, 'etc')
    target_yum_repos_d = os.path.join(target_etc, 'yum.repos.d')
    backup_yum_repos_d = os.path.join(target_etc, 'yum.repos.d.backup')
    if not rhsm.skip_rhsm():
        # TODO: make the _copy_certificates unconditional. keeping it conditional
        # due to issues causing on RHUI
        _copy_certificates(context, target_userspace)
        run(['rm', '-rf', os.path.join(target_etc, 'rhsm')])
        context.copytree_from('/etc/rhsm', os.path.join(target_etc, 'rhsm'))
    # NOTE: we cannot just remove the original target yum.repos.d dir
    # as e.g. in case of RHUI a special RHUI repofiles are installed by a pkg
    # when the target userspace container is created. Removing these files we loose
    # RHUI target repositories. So ...->
    # -> detect such a files...
    with mounting.NspawnActions(base_dir=target_userspace) as target_context:
        files_owned_by_rpms = _get_files_owned_by_rpms(target_context, '/etc/yum.repos.d')

    # -> backup the orig dir & install the new one
    run(['mv', target_yum_repos_d, backup_yum_repos_d])
    context.copytree_from('/etc/yum.repos.d', target_yum_repos_d)

    # -> find old rhui repo files (we have to remove these as they cause duplicates)
    rhui_pkgs = _get_all_rhui_pkgs()
    old_files_owned_by_rhui_rpms = _get_files_owned_by_rpms(context, '/etc/yum.repos.d', rhui_pkgs)
    for fname in old_files_owned_by_rhui_rpms:
        api.current_logger().debug('Remove the old repofile: {}'.format(fname))
        run(['rm', '-f', os.path.join(target_yum_repos_d, fname)])
    # .. continue: remove our leapp rhui repo file (do not care if we are on rhui or not)
    for rhui_map in rhui.gen_rhui_files_map().values():
        for item in rhui_map:
            if item[1] != rhui.YUM_REPOS_PATH:
                continue
            target_leapp_repofile = os.path.join(target_yum_repos_d, item[0])
            if not os.path.isfile(target_leapp_repofile):
                continue
            # we found it!!
            run(['rm', '-f', target_leapp_repofile])
            break

    # -> copy expected files back
    for fname in files_owned_by_rpms:
        api.current_logger().debug('Copy the backed up repo file: {}'.format(fname))
        run(['mv', os.path.join(backup_yum_repos_d, fname), os.path.join(target_yum_repos_d, fname)])

    # -> remove the backed up dir
    run(['rm', '-rf', backup_yum_repos_d])


def _get_product_certificate_path():
    """
    Retrieve the required / used product certificate for RHSM.
    """
    architecture = api.current_actor().configuration.architecture
    target_version = api.current_actor().configuration.version.target
    target_product_type = get_product_type('target')
    certs_dir = api.get_common_folder_path(PROD_CERTS_FOLDER)

    # We do not need any special certificates to reach repos from non-ga channels, only beta requires special cert.
    if target_product_type != 'beta':
        target_product_type = 'ga'

    prod_certs = {
        'x86_64': {
            'ga': '479.pem',
            'beta': '486.pem',
        },
        'aarch64': {
            'ga': '419.pem',
            'beta': '363.pem',
        },
        'ppc64le': {
            'ga': '279.pem',
            'beta': '362.pem',
        },
        's390x': {
            'ga': '72.pem',
            'beta': '433.pem',
        }
    }

    try:
        cert = prod_certs[architecture][target_product_type]
    except KeyError as e:
        raise StopActorExecutionError(message=('Failed to determine what certificate to use for {}.'.format(e)))

    cert_path = os.path.join(certs_dir, target_version, cert)
    if not os.path.isfile(cert_path):
        additional_summary = ''
        if target_product_type != 'ga':
            additional_summary = (
                ' This can happen when upgrading a beta system and the chosen target version does not have'
                ' beta certificates attached (for example, because the GA has been released already).'

            )

        reporting.create_report([
            reporting.Title('Cannot find the product certificate file for the chosen target system.'),
            reporting.Summary(
                'Expected certificate: {cert} with path {path} but it could not be found.{additional}'.format(
                    cert=cert, path=cert_path, additional=additional_summary)
            ),
            reporting.Groups([reporting.Groups.REPOSITORY]),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Remediation(hint=(
                'Set the corresponding target os version in the LEAPP_DEVEL_TARGET_RELEASE environment variable for'
                'which the {cert} certificate is provided'.format(cert=cert)
            )),
        ])
        raise StopActorExecution()

    return cert_path


def _create_target_userspace_directories(target_userspace):
    api.current_logger().debug('Creating target userspace directories.')
    try:
        utils.makedirs(target_userspace)
        api.current_logger().debug('Done creating target userspace directories.')
    except OSError:
        api.current_logger().error(
            'Failed to create temporary target userspace directories %s', target_userspace, 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 {directory} is empty and modifiable.'.format(directory=target_userspace)
            }
        )


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.
    """
    # TODO: this is is duplicate of rhsm._inhibit_on_duplicate_repos
    # Issue: #486
    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 inside the'
            ' "upgrade" container:{0}{1}'
            .format(list_separator_fmt, list_separator_fmt.join(duplicates))
        ),
        reporting.Severity(reporting.Severity.MEDIUM),
        reporting.Groups([reporting.Groups.REPOSITORY]),
        reporting.Groups([reporting.Groups.INHIBITOR]),
        reporting.Remediation(hint=(
            'Remove the duplicate repository definitions or change repoids of'
            ' conflicting repositories on the system to prevent the'
            ' conflict.'
            )
        )
    ])


def _get_all_available_repoids(context):
    repofiles = repofileutils.get_parsed_repofiles(context)
    # TODO: this is not good solution, but keep it as it is now
    # Issue: #486
    if rhsm.skip_rhsm():
        # only if rhsm is skipped, the duplicate repos are not detected
        # automatically and we need to do it extra
        _inhibit_on_duplicate_repos(repofiles)
    repoids = []
    for rfile in repofiles:
        if rfile.data:
            repoids += [repo.repoid for repo in rfile.data]
    return set(repoids)


def _get_rhsm_available_repoids(context):
    target_major_version = get_target_major_version()
    # FIXME: check that required repo IDs (baseos, appstream)
    # + or check that all required RHEL repo IDs are available.
    if rhsm.skip_rhsm():
        return set()
    # Get the RHSM repos available in the target RHEL container
    # TODO: very similar thing should happens for all other repofiles in container
    #
    repoids = rhsm.get_available_repo_ids(context)
    if not repoids or len(repoids) < 2:
        reporting.create_report([
            reporting.Title('Cannot find required basic RHEL target repositories.'),
            reporting.Summary(
                'This can happen when a repository ID was entered incorrectly either while using the --enablerepo'
                ' option of leapp or in a third party actor that produces a CustomTargetRepositoryMessage.'
            ),
            reporting.Groups([reporting.Groups.REPOSITORY]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Remediation(hint=(
                'It is required to have RHEL repositories on the system'
                ' provided by the subscription-manager unless the --no-rhsm'
                ' option is specified. You might be missing a valid SKU for'
                ' the target system or have a failed network connection.'
                ' Check whether your system is attached to a valid SKU that is'
                ' providing RHEL {} repositories.'
                ' If you are using Red Hat Satellite, read the upgrade documentation'
                ' to set up Satellite and the system properly.'

            ).format(target_major_version)),
            reporting.ExternalLink(
                # TODO: How to handle different documentation links for each version?
                url='https://red.ht/preparing-for-upgrade-to-rhel8',
                title='Preparing for the upgrade')
            ])
        raise StopActorExecution()
    return set(repoids)


def _get_rhui_available_repoids(context, cloud_repo):
    repofiles = repofileutils.get_parsed_repofiles(context)

    # TODO: same refactoring as Issue #486?
    _inhibit_on_duplicate_repos(repofiles)
    repoids = []
    for rfile in repofiles:
        if rfile.file == cloud_repo and rfile.data:
            repoids = [repo.repoid for repo in rfile.data]
            repoids.sort()
            break
    return set(repoids)


def _get_rh_available_repoids(context, indata):
    """
    RH repositories are provided either by RHSM or are stored in the expected repo file provided by
    RHUI special packages (every cloud provider has itw own rpm).
    """

    upg_path = rhui.get_upg_path()

    rh_repoids = _get_rhsm_available_repoids(context)

    if indata and indata.rhui_info:
        cloud_repo = os.path.join(
            '/etc/yum.repos.d/', rhui.RHUI_CLOUD_MAP[upg_path][indata.rhui_info.provider]['leapp_pkg_repo']
        )
        rhui_repoids = _get_rhui_available_repoids(context, cloud_repo)
        rh_repoids.update(rhui_repoids)

    return rh_repoids


def gather_target_repositories(context, indata):
    """
    Get available required target repositories and inhibit or raise error if basic checks do not pass.

    In case of repositories provided by Red Hat, it's checked whether the basic
    required repositories are available (or at least defined) in the given
    context. If not, raise StopActorExecutionError.

    For the custom target repositories we expect all of them have to be defined.
    If any custom target repository is missing, raise StopActorExecutionError.

    If any repository is defined multiple times, produce the inhibitor Report
    msg.

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :return: List of target system repoids
    :rtype: List(string)
    """
    rh_available_repoids = _get_rh_available_repoids(context, indata)
    all_available_repoids = _get_all_available_repoids(context)

    target_repoids = []
    missing_custom_repoids = []
    for target_repo in api.consume(TargetRepositories):
        for rhel_repo in target_repo.rhel_repos:
            if rhel_repo.repoid in rh_available_repoids:
                target_repoids.append(rhel_repo.repoid)
            else:
                # TODO: We shall report that the RHEL repos that we deem necessary for
                # the upgrade are not available; but currently it would just print bunch of
                # data every time as we maps EUS and other repositories as well. But these
                # do not have to be necessary available on the target system in the time
                # of the upgrade. Let's skip it for now until it's clear how we will deal
                # with it.
                pass
        for custom_repo in target_repo.custom_repos:
            if custom_repo.repoid in all_available_repoids:
                target_repoids.append(custom_repo.repoid)
            else:
                missing_custom_repoids.append(custom_repo.repoid)
    api.current_logger().debug("Gathered target repositories: {}".format(', '.join(target_repoids)))
    if not target_repoids:
        reporting.create_report([
            reporting.Title('There are no enabled target repositories'),
            reporting.Summary(
                'This can happen when a system is not correctly registered with the subscription manager'
                ' or, when the leapp --no-rhsm option has been used, no custom repositories have been'
                ' passed on the command line.'
            ),
            reporting.Groups([reporting.Groups.REPOSITORY]),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Remediation(hint=(
                'Ensure the system is correctly registered with the subscription manager and that'
                ' the current subscription is entitled to install the requested target version {version}.'
                ' If you used the --no-rhsm option (or the LEAPP_NO_RHSM=1 environment variable is set),'
                ' ensure the custom repository file is provided with'
                ' properly defined repositories and that the --enablerepo option for leapp is set if the'
                ' repositories are defined in any repofiles under the /etc/yum.repos.d/ directory.'
                ' For more information on custom repository files, see the documentation.'
                ' Finally, verify that the "/etc/leapp/files/repomap.json" file is up-to-date.'
            ).format(version=api.current_actor().configuration.version.target)),
            reporting.ExternalLink(
                # TODO: How to handle different documentation links for each version?
                url='https://red.ht/preparing-for-upgrade-to-rhel8',
                title='Preparing for the upgrade'),
            reporting.RelatedResource("file", "/etc/leapp/files/repomap.json"),
            reporting.RelatedResource("file", "/etc/yum.repos.d/")
        ])
        raise StopActorExecution()
    if missing_custom_repoids:
        reporting.create_report([
            reporting.Title('Some required custom target repositories have not been found'),
            reporting.Summary(
                'This can happen when a repository ID was entered incorrectly either'
                ' while using the --enablerepo option of leapp, or in a third party actor that produces a'
                ' CustomTargetRepositoryMessage.\n'
                'The following repositories IDs could not be found in the target configuration:\n'
                '- {}\n'.format('\n- '.join(missing_custom_repoids))
            ),
            reporting.Groups([reporting.Groups.REPOSITORY]),
            reporting.Groups([reporting.Groups.INHIBITOR]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.ExternalLink(
                # TODO: How to handle different documentation links for each version?
                url='https://access.redhat.com/articles/4977891',
                title='Customizing your Red Hat Enterprise Linux in-place upgrade'),
            reporting.Remediation(hint=(
                'Consider using the custom repository file, which is documented in the official'
                ' upgrade documentation. Check whether a repository ID has been'
                ' entered incorrectly with the --enablerepo option of leapp.'
                ' Check the leapp logs to see the list of all available repositories.'
            ))
        ])
        raise StopActorExecution()
    return set(target_repoids)


def _install_custom_repofiles(context, custom_repofiles):
    """
    Install the required custom repository files into the container.

    The repository files are copied from the host into the /etc/yum.repos.d
    directory into the container.

    :param context: the container where the repofiles should be copied
    :type context: mounting.IsolatedActions class
    :param custom_repofiles: list of custom repo files
    :type custom_repofiles: List(CustomTargetRepositoryFile)
    """
    for rfile in custom_repofiles:
        _dst_path = os.path.join('/etc/yum.repos.d', os.path.basename(rfile.file))
        context.copy_to(rfile.file, _dst_path)


def _gather_target_repositories(context, indata, prod_cert_path):
    """
    This is wrapper function to gather the target repoids.

    Probably the function could be partially merged into gather_target_repositories
    and this could be really just wrapper with the switch of certificates.
    I am keeping that for now as it is as interim step.

    :param context: the container where the repofiles should be copied
    :type context: mounting.IsolatedActions class
    :param indata: majority of input data for the actor
    :type indata: class _InputData
    :param prod_cert_path: path where the target product cert is stored
    :type prod_cert_path: string
    """
    rhsm.set_container_mode(context)
    rhsm.switch_certificate(context, indata.rhsm_info, prod_cert_path)
    if indata.rhui_info:
        rhui.copy_rhui_data(context, indata.rhui_info.provider)
    _install_custom_repofiles(context, indata.custom_repofiles)
    return gather_target_repositories(context, indata)


def _copy_files(context, files):
    """
    Copy the files/dirs from the host to the `context` userspace

    :param context: An instance of a mounting.IsolatedActions class
    :type context: mounting.IsolatedActions class
    :param files: list of files that should be copied from the host to the context
    :type files: list of CopyFile
    """
    for file_task in files:
        if not file_task.dst:
            file_task.dst = file_task.src
        if os.path.isdir(file_task.src):
            context.remove_tree(file_task.dst)
            context.copytree_to(file_task.src, file_task.dst)
        else:
            context.copy_to(file_task.src, file_task.dst)


def _get_target_userspace():
    return constants.TARGET_USERSPACE.format(get_target_major_version())


def _create_target_userspace(context, packages, files, target_repoids):
    """Create the target userspace."""
    target_path = _get_target_userspace()
    prepare_target_userspace(context, target_path, target_repoids, list(packages))
    _prep_repository_access(context, target_path)

    with mounting.NspawnActions(base_dir=target_path) as target_context:
        _copy_files(target_context, files)
    dnfplugin.install(_get_target_userspace())

    # and do not forget to set the rhsm into the container mode again
    with mounting.NspawnActions(_get_target_userspace()) as target_context:
        rhsm.set_container_mode(target_context)


@suppress_deprecation(TMPTargetRepositoriesFacts)
def perform():
    # NOTE: this one action is out of unit-tests completely; we do not use
    # in unit tests the LEAPP_DEVEL_SKIP_RHSM envar anymore
    _check_deprecated_rhsm_skip()

    indata = _InputData()
    prod_cert_path = _get_product_certificate_path()
    with overlaygen.create_source_overlay(
            mounts_dir=constants.MOUNTS_DIR,
            scratch_dir=constants.SCRATCH_DIR,
            storage_info=indata.storage_info,
            xfs_info=indata.xfs_info) as overlay:
        with overlay.nspawn() as context:
            # Mount the ISO into the scratch container
            target_iso = next(api.consume(TargetOSInstallationImage), None)
            with mounting.mount_upgrade_iso_to_root_dir(overlay.target, target_iso):
                target_repoids = _gather_target_repositories(context, indata, prod_cert_path)
                _create_target_userspace(context, indata.packages, indata.files, target_repoids)
                # TODO: this is tmp solution as proper one needs significant refactoring
                target_repo_facts = repofileutils.get_parsed_repofiles(context)
                api.produce(TMPTargetRepositoriesFacts(repositories=target_repo_facts))
                # ## TODO ends here
                api.produce(UsedTargetRepositories(
                    repos=[UsedTargetRepository(repoid=repo) for repo in target_repoids]))
                api.produce(TargetUserSpaceInfo(
                    path=_get_target_userspace(),
                    scratch=constants.SCRATCH_DIR,
                    mounts=constants.MOUNTS_DIR))


# repos/system_upgrade/common/actors/targetuserspacecreator/tests/unit_test_targetuserspacecreator.py

import os
from collections import namedtuple

import pytest

from leapp import models, reporting
from leapp.exceptions import StopActorExecution, StopActorExecutionError
from leapp.libraries.actor import userspacegen
from leapp.libraries.common import overlaygen, repofileutils, rhsm
from leapp.libraries.common.config import architecture
from leapp.libraries.common.testutils import CurrentActorMocked, logger_mocked, produce_mocked
from leapp.utils.deprecation import suppress_deprecation

CUR_DIR = os.path.dirname(os.path.abspath(__file__))
_CERTS_PATH = os.path.join(CUR_DIR, '../../../files', userspacegen.PROD_CERTS_FOLDER)
_DEFAULT_CERT_PATH = os.path.join(_CERTS_PATH, '8.1', '479.pem')


@pytest.fixture
def adjust_cwd():
    previous_cwd = os.getcwd()
    os.chdir(os.path.join(CUR_DIR, "../"))
    yield
    os.chdir(previous_cwd)


class MockedMountingBase(object):
    def __init__(self, **dummy_kwargs):
        self.called_copytree_from = []
        self.target = ''

    def copytree_from(self, src, dst):
        self.called_copytree_from.append((src, dst))

    def __call__(self, **dummy_kwarg):
        yield self

    def call(self, *args, **kwargs):
        return {'stdout': ''}

    def nspawn(self):
        return self

    def __enter__(self):
        return self

    def __exit__(self, exception_type, exception_value, traceback):
        pass


@pytest.mark.parametrize('result,dst_ver,arch,prod_type', [
    (os.path.join(_CERTS_PATH, '8.1', '479.pem'), '8.1', architecture.ARCH_X86_64, 'ga'),
    (os.path.join(_CERTS_PATH, '8.1', '419.pem'), '8.1', architecture.ARCH_ARM64, 'ga'),
    (os.path.join(_CERTS_PATH, '8.1', '279.pem'), '8.1', architecture.ARCH_PPC64LE, 'ga'),
    (os.path.join(_CERTS_PATH, '8.2', '479.pem'), '8.2', architecture.ARCH_X86_64, 'ga'),
    (os.path.join(_CERTS_PATH, '8.5', '486.pem'), '8.5', architecture.ARCH_X86_64, 'beta'),
    (os.path.join(_CERTS_PATH, '8.2', '72.pem'), '8.2', architecture.ARCH_S390X, 'ga'),
    (os.path.join(_CERTS_PATH, '8.5', '433.pem'), '8.5', architecture.ARCH_S390X, 'beta'),
])
def test_get_product_certificate_path(monkeypatch, adjust_cwd, result, dst_ver, arch, prod_type):
    envars = {'LEAPP_DEVEL_TARGET_PRODUCT_TYPE': prod_type}
    curr_actor_mocked = CurrentActorMocked(dst_ver=dst_ver, arch=arch, envars=envars)
    monkeypatch.setattr(userspacegen.api, 'current_actor', curr_actor_mocked)
    assert userspacegen._get_product_certificate_path() in result


@suppress_deprecation(models.RequiredTargetUserspacePackages)
def _gen_packages_msgs():
    _cfiles = [
        models.CopyFile(src='/path/src', dst='/path/dst'),
        models.CopyFile(src='/path/foo', dst='/path/bar'),
    ]
    return [
        models.RequiredTargetUserspacePackages(),
        models.RequiredTargetUserspacePackages(packages=['pkgA']),
        models.RequiredTargetUserspacePackages(packages=['pkgB', 'pkgsC']),
        models.RequiredTargetUserspacePackages(packages=['pkgD']),
        models.TargetUserSpacePreupgradeTasks(),
        models.TargetUserSpacePreupgradeTasks(install_rpms=['pkgA']),
        models.TargetUserSpacePreupgradeTasks(install_rpms=['pkgB', 'pkgsC']),
        models.TargetUserSpacePreupgradeTasks(install_rpms=['pkgD', 'pkgE'], copy_files=[_cfiles[0]]),
        models.TargetUserSpacePreupgradeTasks(copy_files=_cfiles),
    ]


_PACKAGES_MSGS = _gen_packages_msgs()
_RHSMINFO_MSG = models.RHSMInfo(attached_skus=['testing-sku'])
_RHUIINFO_MSG = models.RHUIInfo(provider='aws')
_XFS_MSG = models.XFSPresence()
_STORAGEINFO_MSG = models.StorageInfo()
_CTRF_MSGS = [
    models.CustomTargetRepositoryFile(file='rfileA'),
    models.CustomTargetRepositoryFile(file='rfileB'),
]
_SAEE = StopActorExecutionError
_SAE = StopActorExecution


class MockedConsume(object):
    def __init__(self, *args):
        self._msgs = []
        for arg in args:
            if not arg:
                continue
            if isinstance(arg, list):
                self._msgs.extend(arg)
            else:
                self._msgs.append(arg)

    def __call__(self, model):
        return iter([msg for msg in self._msgs if isinstance(msg, model)])


testInData = namedtuple(
    'TestInData', ['pkg_msgs', 'rhsm_info', 'rhui_info', 'xfs', 'storage', 'custom_repofiles']
)


# NOTE: tests cover know new, deprecated, and both ways how to require packages
# that should be installed to create the target userspace. Cases which could be
# removed completely after the drop of the deprecated functionality, are marked
# with the `# dep` str.
@pytest.mark.parametrize('raised,no_rhsm,testdata', [
    # valid cases with RHSM
    (None, '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),
    (None, '0', testInData(_PACKAGES_MSGS[:4], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),   # dep
    (None, '0', testInData(_PACKAGES_MSGS[4:8], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),  # dep
    (None, '0', testInData(_PACKAGES_MSGS[0], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),
    (None, '0', testInData(_PACKAGES_MSGS[4], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),    # dep
    (None, '0', testInData([], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),
    (None, '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, None, _STORAGEINFO_MSG, None)),
    (None, '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, _CTRF_MSGS)),
    (None, '0', testInData(_PACKAGES_MSGS[0], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, _CTRF_MSGS)),
    (None, '0', testInData(_PACKAGES_MSGS[4], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, _CTRF_MSGS)),  # dep
    (None, '0', testInData([], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, _CTRF_MSGS)),
    (None, '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, None, _STORAGEINFO_MSG, _CTRF_MSGS)),

    # valid cases without RHSM (== skip_rhsm)
    (None, '1', testInData(_PACKAGES_MSGS, None, _RHUIINFO_MSG, _XFS_MSG, _STORAGEINFO_MSG, None)),
    (None, '1', testInData(_PACKAGES_MSGS[:4], None, _RHUIINFO_MSG, _XFS_MSG, _STORAGEINFO_MSG, None)),   # dep
    (None, '1', testInData(_PACKAGES_MSGS[4:8], None, _RHUIINFO_MSG, _XFS_MSG, _STORAGEINFO_MSG, None)),  # dep
    (None, '1', testInData(_PACKAGES_MSGS, None, _RHUIINFO_MSG, None, _STORAGEINFO_MSG, None)),
    (None, '1', testInData([], None, _RHUIINFO_MSG, _XFS_MSG, _STORAGEINFO_MSG, None)),
    (None, '1', testInData([], None, _RHUIINFO_MSG, None, _STORAGEINFO_MSG, None)),
    (None, '1', testInData(_PACKAGES_MSGS, None, _RHUIINFO_MSG, _XFS_MSG, _STORAGEINFO_MSG, _CTRF_MSGS)),
    (None, '1', testInData(_PACKAGES_MSGS, None, _RHUIINFO_MSG, None, _STORAGEINFO_MSG, _CTRF_MSGS)),
    (None, '1', testInData([], None, _RHUIINFO_MSG, _XFS_MSG, _STORAGEINFO_MSG, _CTRF_MSGS)),
    (None, '1', testInData([], None, _RHUIINFO_MSG, None, _STORAGEINFO_MSG, _CTRF_MSGS)),

    # no-rhsm but RHSMInfo defined (should be _RHSMINFO_MSG)
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS[0], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG,
                                             None)),
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS[4], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG,
                                             None)),  # dep
    ((_SAEE, 'RHSM is not'), '1', testInData([], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, None)),
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, None, _STORAGEINFO_MSG, None)),
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG,
                                             _CTRF_MSGS)),
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS[0], _RHSMINFO_MSG, None, _XFS_MSG,
                                             _STORAGEINFO_MSG, _CTRF_MSGS)),
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS[4], _RHSMINFO_MSG, None, _XFS_MSG,
                                             _STORAGEINFO_MSG, _CTRF_MSGS)),  # dep
    ((_SAEE, 'RHSM is not'), '1', testInData([], _RHSMINFO_MSG, None, _XFS_MSG, _STORAGEINFO_MSG, _CTRF_MSGS)),
    ((_SAEE, 'RHSM is not'), '1', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, None, _STORAGEINFO_MSG, _CTRF_MSGS)),

    # missing RHSMInfo but it should exist
    # NOTE: should be this Error?!
    ((_SAE, 'RHSM information'), '0', testInData(_PACKAGES_MSGS, None, None, _XFS_MSG, _STORAGEINFO_MSG, None)),
    ((_SAE, 'RHSM information'), '0', testInData(_PACKAGES_MSGS, None, None, None, _STORAGEINFO_MSG, None)),
    ((_SAE, 'RHSM information'), '0', testInData([], None, None, _XFS_MSG, _STORAGEINFO_MSG, None)),
    ((_SAE, 'RHSM information'), '0', testInData([], None, None, None, _STORAGEINFO_MSG, None)),

    # in the end, error when StorageInfo is missing
    ((_SAEE, 'No storage'), '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, _XFS_MSG, None, None)),
    ((_SAEE, 'No storage'), '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, None, None, None)),
    ((_SAEE, 'No storage'), '0', testInData([], _RHSMINFO_MSG, None, _XFS_MSG, None, None)),
    ((_SAEE, 'No storage'), '0', testInData([], _RHSMINFO_MSG, None, None, None, None)),
    ((_SAEE, 'No storage'), '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, _XFS_MSG, None, _CTRF_MSGS)),
    ((_SAEE, 'No storage'), '0', testInData(_PACKAGES_MSGS, _RHSMINFO_MSG, None, None, None, _CTRF_MSGS)),
    ((_SAEE, 'No storage'), '0', testInData([], _RHSMINFO_MSG, None, _XFS_MSG, None, _CTRF_MSGS)),
    ((_SAEE, 'No storage'), '0', testInData([], _RHSMINFO_MSG, None, None, None, _CTRF_MSGS)),
])
def test_consume_data(monkeypatch, raised, no_rhsm, testdata):
    # do not write never into testdata inside the test !!
    xfs = testdata.xfs
    custom_repofiles = testdata.custom_repofiles
    _exp_pkgs = {'dnf', 'dnf-command(config-manager)'}
    _exp_files = []

    def _get_pkgs(msg):
        if isinstance(msg, models.TargetUserSpacePreupgradeTasks):
            return msg.install_rpms
        return msg.packages

    def _get_files(msg):
        if isinstance(msg, models.TargetUserSpacePreupgradeTasks):
            return msg.copy_files
        return []

    def _cfiles2set(cfiles):
        return {(i.src, i.dst) for i in cfiles}

    if isinstance(testdata.pkg_msgs, list):
        for msg in testdata.pkg_msgs:
            _exp_pkgs.update(_get_pkgs(msg))
            _exp_files += _get_files(msg)
    else:
        _exp_pkgs.update(_get_pkgs(testdata.pkg_msgs))
        _exp_files += _get_files(testdata.pkg_msgs)
    mocked_consume = MockedConsume(testdata.pkg_msgs,
                                   testdata.rhsm_info,
                                   testdata.rhui_info,
                                   xfs,
                                   testdata.storage,
                                   custom_repofiles)

    monkeypatch.setattr(userspacegen.api, 'consume', mocked_consume)
    monkeypatch.setattr(userspacegen.api, 'current_logger', logger_mocked())
    monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked(envars={'LEAPP_NO_RHSM': no_rhsm}))
    if not xfs:
        xfs = models.XFSPresence()
    if not custom_repofiles:
        custom_repofiles = []
    if not raised:
        result = userspacegen._InputData()
        assert result.packages == _exp_pkgs
        assert _cfiles2set(result.files) == _cfiles2set(_exp_files)
        assert result.rhsm_info == testdata.rhsm_info
        assert result.rhui_info == testdata.rhui_info
        assert result.xfs_info == xfs
        assert result.storage_info == testdata.storage
        assert result.custom_repofiles == custom_repofiles
        assert not userspacegen.api.current_logger.warnmsg
        assert not userspacegen.api.current_logger.errmsg
    else:
        with pytest.raises(raised[0]) as err:
            userspacegen._InputData()
        if isinstance(err.value, StopActorExecutionError):
            assert raised[1] in err.value.message
        else:
            assert userspacegen.api.current_logger.warnmsg
            assert any([raised[1] in x for x in userspacegen.api.current_logger.warnmsg])


@pytest.mark.skip(reason="Currently not implemented in the actor. It's TODO.")
def test_gather_target_repositories(monkeypatch):
    monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked())
    # The available RHSM repos
    monkeypatch.setattr(rhsm, 'get_available_repo_ids', lambda x: ['repoidX', 'repoidY', 'repoidZ'])
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    # The required RHEL repos based on the repo mapping and PES data + custom repos required by third party actors
    monkeypatch.setattr(userspacegen.api, 'consume', lambda x: iter([models.TargetRepositories(
        rhel_repos=[models.RHELTargetRepository(repoid='repoidX'),
                    models.RHELTargetRepository(repoid='repoidY')],
        custom_repos=[models.CustomTargetRepository(repoid='repoidCustom')])]))

    target_repoids = userspacegen.gather_target_repositories(None, None)

    assert target_repoids == ['repoidX', 'repoidY', 'repoidCustom']


def test_gather_target_repositories_none_available(monkeypatch):

    mocked_produce = produce_mocked()
    monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(userspacegen.api.current_actor(), 'produce', mocked_produce)
    monkeypatch.setattr(rhsm, 'get_available_repo_ids', lambda x: [])
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    with pytest.raises(StopActorExecution):
        userspacegen.gather_target_repositories(None, None)
        assert mocked_produce.called
        reports = [m.report for m in mocked_produce.model_instances if isinstance(m, reporting.Report)]
        inhibitors = [m for m in reports if 'INHIBITOR' in m.get('flags', ())]
        assert len(inhibitors) == 1
        assert inhibitors[0].get('title', '') == 'Cannot find required basic RHEL target repositories.'


def test_gather_target_repositories_rhui(monkeypatch):

    indata = testInData(
        _PACKAGES_MSGS, _RHSMINFO_MSG, _RHUIINFO_MSG, _XFS_MSG, _STORAGEINFO_MSG, None
    )

    monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(userspacegen, '_get_all_available_repoids', lambda x: [])
    monkeypatch.setattr(
        userspacegen, '_get_rh_available_repoids', lambda x, y: ['rhui-1', 'rhui-2', 'rhui-3']
    )
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: True)
    monkeypatch.setattr(
        userspacegen.api, 'consume', lambda x: iter(
            [models.TargetRepositories(
                rhel_repos=[
                    models.RHELTargetRepository(repoid='rhui-1'),
                    models.RHELTargetRepository(repoid='rhui-2')
                ]
            )
            ])
    )
    target_repoids = userspacegen.gather_target_repositories(None, indata)
    assert target_repoids == set(['rhui-1', 'rhui-2'])


@pytest.mark.skip(reason="Currently not implemented in the actor. It's TODO.")
def test_gather_target_repositories_required_not_available(monkeypatch):
    # If the repos that Leapp identifies as required for the upgrade (based on the repo mapping and PES data) are not
    # available, an exception shall be raised

    mocked_produce = produce_mocked()
    monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(userspacegen.api.current_actor(), 'produce', mocked_produce)
    # The available RHSM repos
    monkeypatch.setattr(rhsm, 'get_available_repo_ids', lambda x: ['repoidA', 'repoidB', 'repoidC'])
    monkeypatch.setattr(rhsm, 'skip_rhsm', lambda: False)
    # The required RHEL repos based on the repo mapping and PES data + custom repos required by third party actors
    monkeypatch.setattr(userspacegen.api, 'consume', lambda x: iter([models.TargetRepositories(
        rhel_repos=[models.RHELTargetRepository(repoid='repoidX'),
                    models.RHELTargetRepository(repoid='repoidY')],
        custom_repos=[models.CustomTargetRepository(repoid='repoidCustom')])]))

    with pytest.raises(StopActorExecution):
        userspacegen.gather_target_repositories(None)
        assert mocked_produce.called
        reports = [m.report for m in mocked_produce.model_instances if isinstance(m, reporting.Report)]
        inhibitors = [m for m in reports if 'INHIBITOR' in m.get('flags', ())]
        assert len(inhibitors) == 1
        assert inhibitors[0].get('title', '') == 'Cannot find required basic RHEL target repositories.'


def mocked_consume_data():
    packages = {'dnf', 'dnf-command(config-manager)', 'pkgA', 'pkgB'}
    rhsm_info = _RHSMINFO_MSG
    rhui_info = _RHUIINFO_MSG
    xfs_info = models.XFSPresence()
    storage_info = models.StorageInfo()
    custom_repofiles = []
    files = set()
    fields = [
        'packages',
        'rhsm_info',
        'rhui_info',
        'xfs_info',
        'storage_info',
        'custom_repofiles',
        'files'
    ]

    return namedtuple('TestInData', fields)(
                packages,
                rhsm_info,
                rhui_info,
                xfs_info,
                storage_info,
                custom_repofiles,
                files,
    )


# TODO: come up with additional tests for the main function
def test_perform_ok(monkeypatch):
    repoids = ['repoidX', 'repoidY']
    monkeypatch.setattr(userspacegen, '_InputData', mocked_consume_data)
    monkeypatch.setattr(userspacegen, '_get_product_certificate_path', lambda: _DEFAULT_CERT_PATH)
    monkeypatch.setattr(overlaygen, 'create_source_overlay', MockedMountingBase)
    monkeypatch.setattr(userspacegen, '_gather_target_repositories', lambda *x: repoids)
    monkeypatch.setattr(userspacegen, '_create_target_userspace', lambda *x: None)
    monkeypatch.setattr(userspacegen.api, 'current_actor', CurrentActorMocked())
    monkeypatch.setattr(userspacegen.api, 'produce', produce_mocked())
    monkeypatch.setattr(repofileutils, 'get_repodirs', lambda: ['/etc/yum.repos.d'])
    userspacegen.perform()
    msg_target_repos = models.UsedTargetRepositories(
        repos=[models.UsedTargetRepository(repoid=repo) for repo in repoids])
    assert userspacegen.api.produce.called == 3
    assert isinstance(userspacegen.api.produce.model_instances[0], models.TMPTargetRepositoriesFacts)
    assert userspacegen.api.produce.model_instances[1] == msg_target_repos
    # this one is full of constants, so it's safe to check just the instance
    assert isinstance(userspacegen.api.produce.model_instances[2], models.TargetUserSpaceInfo)


# repos/system_upgrade/common/actors/transactionworkarounds/libraries/transactionworkarounds.py

import os

from leapp.libraries.stdlib import api
from leapp.models import RpmTransactionTasks


def process():
    location = api.get_folder_path('bundled-rpms')
    local_rpms = []
    for name in os.listdir(location):
        if name.endswith('.rpm'):
            # It is important to put here the realpath to the files here, because
            # symlinks cannot be resolved properly inside of the target userspace since they use the /installroot
            # mount target
            local_rpms.append(os.path.realpath(os.path.join(location, name)))
    if local_rpms:
        api.produce(RpmTransactionTasks(local_rpms=local_rpms))


# repos/system_upgrade/common/actors/transactionworkarounds/tests/test_transaction_workarounds_transactionworkarounds.py

from leapp.libraries.actor import transactionworkarounds as tw
from leapp.models import RpmTransactionTasks


def test_transactionworkarounds_2rpms(monkeypatch):
    result = []
    monkeypatch.setattr(tw.os, 'listdir', lambda _: ('abc.rpm', 'def.rpm', 'random.file'),)
    monkeypatch.setattr(tw.api, 'get_folder_path', lambda _: '/FAKE/FOLDER/PATH')
    monkeypatch.setattr(tw.api, 'produce', lambda *models: result.extend(models))

    tw.process()

    assert result
    assert isinstance(result[0], RpmTransactionTasks)
    assert result[0].local_rpms
    assert result[0].local_rpms == ['/FAKE/FOLDER/PATH/abc.rpm', '/FAKE/FOLDER/PATH/def.rpm']
    assert not result[0].to_install
    assert not result[0].to_remove
    assert not result[0].to_keep


def test_transactionworkarounds_0rpms(monkeypatch):
    result = []
    monkeypatch.setattr(tw.os, 'listdir', lambda _: ('abc.not', 'def.duh', 'random.file'),)
    monkeypatch.setattr(tw.api, 'get_folder_path', lambda _: '/FAKE/FOLDER/PATH')
    monkeypatch.setattr(tw.api, 'produce', lambda *models: result.extend(models))
    tw.process()
    assert not result


# repos/system_upgrade/common/actors/udev/udevadminfo/actor.py

from leapp.actors import Actor
from leapp.libraries.stdlib import run
from leapp.models import UdevAdmInfoData
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class UdevAdmInfo(Actor):
    """
    Produces data exported by the "udevadm info" command.
    """

    name = 'udevadm_info'
    consumes = ()
    produces = (UdevAdmInfoData,)
    tags = (IPUWorkflowTag, FactsPhaseTag,)

    def process(self):
        out = run(['udevadm', 'info', '-e'])['stdout']
        self.produce(UdevAdmInfoData(db=out))


# repos/system_upgrade/common/actors/updategrubcore/libraries/updategrubcore.py

from leapp import reporting
from leapp.exceptions import StopActorExecution
from leapp.libraries.stdlib import api, CalledProcessError, config, run


def update_grub_core(grub_dev):
    """
    Update GRUB core after upgrade from RHEL7 to RHEL8

    On legacy systems, GRUB core does not get automatically updated when GRUB packages
    are updated.
    """
    cmd = ['grub2-install', grub_dev]
    if config.is_debug():
        cmd += ['-v']
    try:
        run(cmd)
    except CalledProcessError as err:
        reporting.create_report([
            reporting.Title('GRUB core update failed'),
            reporting.Summary(str(err)),
            reporting.Groups([reporting.Groups.BOOT]),
            reporting.Severity(reporting.Severity.HIGH),
            reporting.Remediation(
                hint='Please run "grub2-install <GRUB_DEVICE>" manually after upgrade'
            )
        ])
        api.current_logger().warning('GRUB core update on {} failed'.format(grub_dev))
        raise StopActorExecution()
    reporting.create_report([
        reporting.Title('GRUB core successfully updated'),
        reporting.Summary('GRUB core on {} was successfully updated'.format(grub_dev)),
        reporting.Groups([reporting.Groups.BOOT]),
        reporting.Severity(reporting.Severity.INFO)
    ])


# repos/system_upgrade/common/actors/updategrubcore/tests/test_updategrubcore.py

import pytest

from leapp import reporting
from leapp.exceptions import StopActorExecution
from leapp.libraries.actor import updategrubcore
from leapp.libraries.common import testutils
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import UpdateGrub
from leapp.reporting import Report

UPDATE_OK_TITLE = 'GRUB core successfully updated'
UPDATE_FAILED_TITLE = 'GRUB core update failed'


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


class run_mocked(object):
    def __init__(self, raise_err=False):
        self.called = 0
        self.args = []
        self.raise_err = raise_err

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


def test_update_grub(monkeypatch):
    monkeypatch.setattr(api, 'consume', lambda x: iter([UpdateGrub(grub_device='/dev/vda')]))
    monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
    monkeypatch.setattr(updategrubcore, 'run', run_mocked())
    updategrubcore.update_grub_core('/dev/vda')
    assert reporting.create_report.called
    assert UPDATE_OK_TITLE == reporting.create_report.report_fields['title']


def test_update_grub_failed(monkeypatch):
    monkeypatch.setattr(api, 'consume', lambda x: iter([UpdateGrub(grub_device='/dev/vda')]))
    monkeypatch.setattr(reporting, "create_report", testutils.create_report_mocked())
    monkeypatch.setattr(updategrubcore, 'run', run_mocked(raise_err=True))
    with pytest.raises(StopActorExecution):
        updategrubcore.update_grub_core('/dev/vda')
    assert reporting.create_report.called
    assert UPDATE_FAILED_TITLE == reporting.create_report.report_fields['title']


def test_update_grub_negative(current_actor_context):
    current_actor_context.run()
    assert not current_actor_context.consume(Report)


# repos/system_upgrade/common/actors/usedrepositoriesscanner/tests/test_usedrepositoriesscanner.py

from leapp.models import (
    InstalledRedHatSignedRPM,
    RepositoriesFacts,
    RepositoryData,
    RepositoryFile,
    RPM,
    UsedRepositories
)
from leapp.snactor.fixture import current_actor_context


def get_sample_rpm(name, repository):
    return RPM(
        name=name,
        epoch='1',
        packager='Red Hat Inc.',
        version='0.0.1',
        release='1.el7',
        arch='x86_64',
        pgpsig='RSA/SHA256, Mon 01 Jan 1970 00:00:00 AM UTC, Key ID 0123456789abcdef',
        repository=repository)


def get_sample_installed_pkgs(pkgs):
    return InstalledRedHatSignedRPM(items=[get_sample_rpm(*p) for p in pkgs])


def get_sample_repository(repoid, name):
    return RepositoryFile(file='/etc/yum.d/sample.repo', data=[RepositoryData(
        repoid=repoid,
        name=name,
        enabled=True)])


def get_sample_repositories(repos):
    return RepositoriesFacts(
        repositories=[get_sample_repository(*r) for r in repos])


def test_actor_execution(current_actor_context):
    installed = get_sample_installed_pkgs([
        ('pkg1', 'rhel-7-server-rpms'),
        ('pkg2', 'rhel-7-server-rpms')])

    repos = get_sample_repositories([
        ('rhel-7-server-rpms', 'RHEL 7 Server'),
        ('rhel-7-unused-rpms', 'RHEL 7 Unused')])

    current_actor_context.feed(installed)
    current_actor_context.feed(repos)
    current_actor_context.run()
    assert current_actor_context.consume(UsedRepositories)
    used_repos = current_actor_context.consume(UsedRepositories)[0].repositories
    assert len(used_repos) == 1
    assert used_repos[0].repository == 'rhel-7-server-rpms'
    assert len(used_repos[0].packages) == 2
    assert used_repos[0].packages == ['pkg1', 'pkg2']


# repos/system_upgrade/common/actors/verifycheckresults/libraries/verifycheckresults.py

from leapp.exceptions import RequestStopAfterPhase
from leapp.libraries.stdlib import api
from leapp.reporting import Report
from leapp.utils.report import is_inhibitor


def check():
    if [msg for msg in api.consume(Report) if is_inhibitor(msg.report)]:
        raise RequestStopAfterPhase()


# repos/system_upgrade/common/actors/verifycheckresults/tests/unit_test_verifycheckresults.py

from collections import namedtuple

import pytest

from leapp.exceptions import RequestStopAfterPhase
from leapp.libraries.actor import verifycheckresults
from leapp.libraries.stdlib import api


class Report(object):
    def __init__(self, message):
        self.message = message

    def get(self, _, __):
        if self.message == 'title_with_inhibitor':
            return ['inhibitor']
        return []

    def __getitem__(self, _):
        return self.message


def test_actor(monkeypatch):
    def report_mocked(*models):
        yield namedtuple('msg', ['report'])(Report('title_with_inhibitor'))

    monkeypatch.setattr(api, "consume", report_mocked)

    with pytest.raises(RequestStopAfterPhase):
        verifycheckresults.check()


def test_actor_no_inhibitor(monkeypatch):
    def report_mocked(*models):
        yield namedtuple('msg', ['report'])(Report('title_without_inhibitor'))

    monkeypatch.setattr(api, "consume", report_mocked)
    verifycheckresults.check()


# repos/system_upgrade/common/actors/verifydialogs/libraries/verifydialogs.py

from leapp import reporting
from leapp.libraries.stdlib import api
from leapp.models import DialogModel


def check_dialogs(inhibit_if_no_userchoice=True):
    results = list(api.consume(DialogModel))
    for dialog in results:
        sections = dialog.answerfile_sections
        summary = ('One or more sections in answerfile are missing user choices: {}\n'
                   'For more information consult https://leapp.readthedocs.io/en/latest/dialogs.html')
        dialog_resources = [reporting.RelatedResource('dialog', s) for s in sections]
        dialogs_remediation = ('Please register user choices with leapp answer cli command or by manually editing '
                               'the answerfile.')
        # FIXME: Enable more choices once we can do multi-command remediations
        cmd_remediation = [['leapp', 'answer', '--section', "{}={}".format(s, choice)]
                           for s, choices in dialog.answerfile_sections.items() for choice in choices[:1]]
        report_data = [reporting.Title('Missing required answers in the answer file'),
                       reporting.Severity(reporting.Severity.HIGH),
                       reporting.Summary(summary.format('\n'.join(sections))),
                       reporting.Groups([reporting.Groups.INHIBITOR] if inhibit_if_no_userchoice else []),
                       reporting.Remediation(hint=dialogs_remediation, commands=cmd_remediation),
                       reporting.Key(dialog.key)]
        reporting.create_report(report_data + dialog_resources)


# repos/system_upgrade/common/actors/xfsinfoscanner/libraries/xfsinfoscanner.py

from leapp.libraries.stdlib import api, run
from leapp.models import StorageInfo, XFSPresence


def scan_xfs_fstab(data):
    mountpoints = set()
    for entry in data:
        if entry.fs_vfstype == "xfs":
            mountpoints.add(entry.fs_file)

    return mountpoints


def scan_xfs_mount(data):
    mountpoints = set()
    for entry in data:
        if entry.tp == "xfs":
            mountpoints.add(entry.mount)

    return mountpoints


def is_xfs_without_ftype(mp):
    for l in run(['/usr/sbin/xfs_info', '{}'.format(mp)], split=True)['stdout']:
        if 'ftype=0' in l:
            return True

    return False


def scan_xfs():
    storage_info_msgs = api.consume(StorageInfo)
    storage_info = next(storage_info_msgs, None)

    if list(storage_info_msgs):
        api.current_logger().warning('Unexpectedly received more than one StorageInfo message.')

    fstab_data = set()
    mount_data = set()
    if storage_info:
        fstab_data = scan_xfs_fstab(storage_info.fstab)
        mount_data = scan_xfs_mount(storage_info.mount)

    mountpoints = fstab_data | mount_data
    mountpoints_ftype0 = list(filter(is_xfs_without_ftype, mountpoints))

    # By now, we only have XFS mountpoints and check whether or not it has ftype = 0
    api.produce(XFSPresence(
        present=len(mountpoints) > 0,
        without_ftype=len(mountpoints_ftype0) > 0,
        mountpoints_without_ftype=mountpoints_ftype0,
    ))


# repos/system_upgrade/common/actors/xfsinfoscanner/tests/unit_test_xfsinfoscanner.py

from leapp.libraries.actor import xfsinfoscanner
from leapp.libraries.common.testutils import produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import FstabEntry, MountEntry, StorageInfo, SystemdMountEntry, XFSPresence


class run_mocked(object):
    def __init__(self):
        self.called = 0
        self.args = None

    def __call__(self, args, split=True):
        self.called += 1
        self.args = args

        with_ftype = {'stdout': [
            "meta-data=/dev/loop0             isize=512    agcount=4, agsize=131072 blks",
            "         =                       sectsz=512   attr=2, projid32bit=1",
            "         =                       crc=1        finobt=0 spinodes=0",
            "data     =                       bsize=4096   blocks=524288, imaxpct=25",
            "         =                       sunit=0      swidth=0 blks",
            "naming   =version 2              bsize=4096   ascii-ci=0 ftype=1",
            "log      =internal               bsize=4096   blocks=2560, version=2",
            "         =                       sectsz=512   sunit=0 blks, lazy-count=1",
            "realtime =none                   extsz=4096   blocks=0, rtextents=0"]}

        without_ftype = {'stdout': [
            "meta-data=/dev/loop0             isize=512    agcount=4, agsize=131072 blks",
            "         =                       sectsz=512   attr=2, projid32bit=1",
            "         =                       crc=1        finobt=0 spinodes=0",
            "data     =                       bsize=4096   blocks=524288, imaxpct=25",
            "         =                       sunit=0      swidth=0 blks",
            "naming   =version 2              bsize=4096   ascii-ci=0 ftype=0",
            "log      =internal               bsize=4096   blocks=2560, version=2",
            "         =                       sectsz=512   sunit=0 blks, lazy-count=1",
            "realtime =none                   extsz=4096   blocks=0, rtextents=0"]}

        if "/var" in self.args:
            return without_ftype

        return with_ftype


def test_scan_xfs_fstab(monkeypatch):
    fstab_data_no_xfs = {
        "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"}

    mountpoints = xfsinfoscanner.scan_xfs_fstab([FstabEntry(**fstab_data_no_xfs)])
    assert not mountpoints

    fstab_data_xfs = {
        "fs_spec": "/dev/mapper/rhel-root",
        "fs_file": "/",
        "fs_vfstype": "xfs",
        "fs_mntops": "defaults",
        "fs_freq": "0",
        "fs_passno": "0"}

    mountpoints = xfsinfoscanner.scan_xfs_fstab([FstabEntry(**fstab_data_xfs)])
    assert mountpoints == {"/"}


def test_scan_xfs_mount(monkeypatch):
    mount_data_no_xfs = {
        "name": "tmpfs",
        "mount": "/run/snapd/ns",
        "tp": "tmpfs",
        "options": "rw,nosuid,nodev,seclabel,mode=755"}

    mountpoints = xfsinfoscanner.scan_xfs_mount([MountEntry(**mount_data_no_xfs)])
    assert not mountpoints

    mount_data_xfs = {
        "name": "/dev/vda1",
        "mount": "/boot",
        "tp": "xfs",
        "options": "rw,relatime,seclabel,attr2,inode64,noquota"}

    mountpoints = xfsinfoscanner.scan_xfs_mount([MountEntry(**mount_data_xfs)])
    assert mountpoints == {"/boot"}


def test_is_xfs_without_ftype(monkeypatch):
    monkeypatch.setattr(xfsinfoscanner, "run", run_mocked())

    assert xfsinfoscanner.is_xfs_without_ftype("/var")
    assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /var"

    assert not xfsinfoscanner.is_xfs_without_ftype("/boot")
    assert ' '.join(xfsinfoscanner.run.args) == "/usr/sbin/xfs_info /boot"


def test_scan_xfs(monkeypatch):
    monkeypatch.setattr(xfsinfoscanner, "run", run_mocked())

    def consume_no_xfs_message_mocked(*models):
        yield StorageInfo()

    monkeypatch.setattr(api, "consume", consume_no_xfs_message_mocked)
    monkeypatch.setattr(api, "produce", produce_mocked())

    xfsinfoscanner.scan_xfs()
    assert api.produce.called == 1
    assert len(api.produce.model_instances) == 1
    assert isinstance(api.produce.model_instances[0], XFSPresence)
    assert not api.produce.model_instances[0].present
    assert not api.produce.model_instances[0].without_ftype
    assert not api.produce.model_instances[0].mountpoints_without_ftype

    def consume_ignored_xfs_message_mocked(*models):
        mount_data = {
            "name": "/dev/vda1",
            "mount": "/boot",
            "tp": "xfs",
            "options": "rw,relatime,seclabel,attr2,inode64,noquota"}
        yield StorageInfo(mount=[MountEntry(**mount_data)])

    monkeypatch.setattr(api, "consume", consume_ignored_xfs_message_mocked)
    monkeypatch.setattr(api, "produce", produce_mocked())

    xfsinfoscanner.scan_xfs()
    assert api.produce.called == 1
    assert len(api.produce.model_instances) == 1
    assert isinstance(api.produce.model_instances[0], XFSPresence)
    assert api.produce.model_instances[0].present
    assert not api.produce.model_instances[0].without_ftype
    assert not api.produce.model_instances[0].mountpoints_without_ftype

    def consume_xfs_with_ftype_message_mocked(*models):
        fstab_data = {
            "fs_spec": "/dev/mapper/rhel-root",
            "fs_file": "/",
            "fs_vfstype": "xfs",
            "fs_mntops": "defaults",
            "fs_freq": "0",
            "fs_passno": "0"}
        yield StorageInfo(fstab=[FstabEntry(**fstab_data)])

    monkeypatch.setattr(api, "consume", consume_xfs_with_ftype_message_mocked)
    monkeypatch.setattr(api, "produce", produce_mocked())

    xfsinfoscanner.scan_xfs()
    assert api.produce.called == 1
    assert len(api.produce.model_instances) == 1
    assert isinstance(api.produce.model_instances[0], XFSPresence)
    assert api.produce.model_instances[0].present
    assert not api.produce.model_instances[0].without_ftype
    assert not api.produce.model_instances[0].mountpoints_without_ftype

    def consume_xfs_without_ftype_message_mocked(*models):
        fstab_data = {
            "fs_spec": "/dev/mapper/rhel-root",
            "fs_file": "/var",
            "fs_vfstype": "xfs",
            "fs_mntops": "defaults",
            "fs_freq": "0",
            "fs_passno": "0"}
        yield StorageInfo(fstab=[FstabEntry(**fstab_data)])

    monkeypatch.setattr(api, "consume", consume_xfs_without_ftype_message_mocked)
    monkeypatch.setattr(api, "produce", produce_mocked())

    xfsinfoscanner.scan_xfs()
    assert api.produce.called == 1
    assert len(api.produce.model_instances) == 1
    assert isinstance(api.produce.model_instances[0], XFSPresence)
    assert api.produce.model_instances[0].present
    assert api.produce.model_instances[0].without_ftype
    assert api.produce.model_instances[0].mountpoints_without_ftype
    assert len(api.produce.model_instances[0].mountpoints_without_ftype) == 1
    assert api.produce.model_instances[0].mountpoints_without_ftype[0] == '/var'

    def consume_no_message_mocked(*models):
        yield None

    monkeypatch.setattr(api, "consume", consume_no_message_mocked)
    monkeypatch.setattr(api, "produce", produce_mocked())

    xfsinfoscanner.scan_xfs()
    assert api.produce.called == 1
    assert len(api.produce.model_instances) == 1
    assert isinstance(api.produce.model_instances[0], XFSPresence)
    assert not api.produce.model_instances[0].present
    assert not api.produce.model_instances[0].without_ftype
    assert not api.produce.model_instances[0].mountpoints_without_ftype


# 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 logging
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" becomes "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', ())

        available_modules_to_enable = []
        unavailable_modules = []
        for module in modules_to_enable:
            matching_modules, dummy_nsvcap = module_base.get_modules(module)
            target_bucket = available_modules_to_enable if matching_modules else unavailable_modules
            target_bucket.append(module)

        if unavailable_modules:
            dnf_plugin_logger = logging.getLogger('dnf.plugin')
            msg = 'The following modules were requested to be enabled, but they are unavailable: %s'
            dnf_plugin_logger.warning(msg, ', '.join(unavailable_modules))

        # 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=[available_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 api, CalledProcessError


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 prevent 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_target_major_version, get_target_version
from leapp.libraries.stdlib import api, CalledProcessError, config
from leapp.models import DNFWorkaround

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_workarounds(context=None):
    """
    Apply registered workarounds in the given context environment
    """
    context = context or mounting.NotIsolatedActions(base_dir='/')
    for workaround in api.consume(DNFWorkaround):
        try:
            api.show_message('Applying transaction workaround - {}'.format(workaround.display_name))
            if workaround.script_args:
                cmd_str = '{script} {args}'.format(
                    script=workaround.script_path,
                    args=' '.join(workaround.script_args)
                )
            else:
                cmd_str = workaround.script_path
            context.call(['/bin/bash', '-c', cmd_str])
        except (OSError, CalledProcessError) as e:
            raise StopActorExecutionError(
                message=('Failed to execute script to apply transaction workaround {display_name}.'
                         ' Message: {error}'.format(error=str(e), display_name=workaround.display_name))
            )


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 upgrade
        # 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, target_iso=None):
    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:
            with mounting.mount_upgrade_iso_to_root_dir(target_userspace_info.path, target_iso):
                yield context, overlay, target_repoids


def perform_transaction_check(target_userspace_info,
                              used_repos,
                              tasks,
                              xfs_info,
                              storage_info,
                              plugin_info,
                              target_iso=None):
    """
    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, target_iso=target_iso) as (context, overlay, target_repoids):
        apply_workarounds(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,
                         target_iso=None,
                         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,
                          target_iso=target_iso) as (context, overlay, target_repoids):
        apply_workarounds(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,
                    target_iso=None,
                    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,
                          target_iso=target_iso) as (context, overlay, target_repoids):
        apply_workarounds(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

import os
from re import compile as regexp

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 io  # Python2/Python3 compatible IO (open etc.)
import os

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.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import CalledProcessError


@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 connection ({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 os
import warnings

from leapp.libraries.common.config.version import get_source_major_version

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:
        # The DNF command reads /etc/yum/vars/releasever, but the DNF library does not. It parses redhat-release
        # package to retrieve system's major version which it then uses as $releasever. However, some systems might
        # have repositories only for the exact system version (including the minor number). In a case when
        # /etc/yum/vars/releasever is present, read its contents so that we can access repositores on such systems.
        conf = dnf.conf.Conf()
        pkg_manager = 'yum' if get_source_major_version() == '7' else 'dnf'
        releasever_path = '/etc/{0}/vars/releasever'.format(pkg_manager)
        if os.path.exists(releasever_path):
            with open(releasever_path) as releasever_file:
                releasever = releasever_file.read().strip()
                conf.substitutions['releasever'] = releasever
        else:
            conf.substitutions['releasever'] = get_source_major_version()

        base = dnf.Base(conf=conf)
        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.common.config import get_all_envs
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import api, CalledProcessError, run

# Using ALWAYS_BIND will crash the upgrade process if the file does not exist.
# Consider instead adding an entry to the ScanFilesToCopyIntoTargetSystem actor that
# conditionally (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)
        ]


def mount_upgrade_iso_to_root_dir(root_dir, target_iso):
    """
    Context manager mounting the target RHEL ISO into the system root residing at `root_dir`.

    If the `target_iso` is None no action is performed.

    :param root_dir: Path to a directory containing a system root.
    :type root_dir: str
    :param target_iso: Description of the ISO to be mounted.
    :type target_iso: Optional[TargetOSInstallationImage]
    :rtype: Optional[LoopMount]
    """
    if not target_iso:
        return NullMount(root_dir)

    mountpoint = target_iso.mountpoint[1:]  # Strip the leading / from the absolute mountpoint
    mountpoint_in_root_dir = os.path.join(root_dir, mountpoint)
    return LoopMount(source=target_iso.path, target=mountpoint_in_root_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 api, CalledProcessError, 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 constraints.
    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 fields, RepositoryData, RepositoryFile

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 api, CalledProcessError
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.Groups([reporting.Groups.REPOSITORY]),
        reporting.Groups([reporting.Groups.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-apps': {
            'src_pkg': 'rhui-azure-rhel7-base-sap-apps',
            'target_pkg': 'rhui-azure-rhel8-sapapps',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure-sap',
            'leapp_pkg_repo': 'leapp-azure-sap-apps.repo',
            'files_map': [
                ('content-rhel8-eus.crt', RHUI_PKI_PRODUCT_DIR),
                ('content-rhel8-sapapps.crt', RHUI_PKI_PRODUCT_DIR),
                ('key-rhel8-eus.pem', RHUI_PKI_DIR),
                ('key-rhel8-sapapps.pem', RHUI_PKI_DIR),
                ('leapp-azure-sap-apps.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)
            ],
        },
        'google': {
            'src_pkg': 'google-rhui-client-rhel7',
            'target_pkg': 'google-rhui-client-rhel8',
            'leapp_pkg': 'leapp-rhui-google',
            'leapp_pkg_repo': 'leapp-google.repo',
            'files_map': [
                ('content.crt', RHUI_PKI_PRODUCT_DIR),
                ('key.pem', RHUI_PKI_DIR),
                ('leapp-google.repo', YUM_REPOS_PATH)
            ],
        },
        'google-sap': {
            'src_pkg': 'google-rhui-client-rhel79-sap',
            'target_pkg': 'google-rhui-client-rhel8-sap',
            'leapp_pkg': 'leapp-rhui-google-sap',
            'leapp_pkg_repo': 'leapp-google-sap.repo',
            'files_map': [
                ('content.crt', RHUI_PKI_PRODUCT_DIR),
                ('key.pem', RHUI_PKI_DIR),
                ('leapp-google-sap.repo', YUM_REPOS_PATH)
            ],
        },
    },
    '8to9': {
        '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-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)
            ],
        },
        'aws-sap-e4s': {
            'src_pkg': 'rh-amazon-rhui-client-sap-bundle-e4s',
            '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-bundle-e4s.crt', RHUI_PKI_PRODUCT_DIR),
                ('content-rhel9-sap-bundle-e4s.key', RHUI_PKI_DIR),
                ('cdn.redhat.com-chain.crt', RHUI_PKI_DIR),
                ('leapp-aws-sap-e4s.repo', YUM_REPOS_PATH)
            ],
        },
        '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)
            ],
        },
        # FIXME(mhecko): This entry is identical to the azure one, since we have no EUS content yet, therefore, it
        # #              serves only the purpose of containing the name of rhui client package to correctly detect
        # #              cloud provider. Trying to work around this entry by specifying --channel, will result in
        # #              failures - there is no repomapping for EUS content, and the name of target pkg differs on EUS.
        # #              If the EUS image is available sooner than the 'azure-eus' entry gets modified, the user can
        # #              still upgrade to non-EUS, and switch the newly upgraded system to EUS manually.
        'azure-eus': {
            'src_pkg': 'rhui-azure-rhel8-eus',
            'target_pkg': 'rhui-azure-rhel9',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure-eus',
            '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-rhel8-sap-ha',
            'target_pkg': 'rhui-azure-rhel9-sap-ha',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure-sap',
            'leapp_pkg_repo': 'leapp-azure-sap-ha.repo',
            'files_map': [
                ('content-sap-ha.crt', RHUI_PKI_PRODUCT_DIR),
                ('key-sap-ha.pem', RHUI_PKI_DIR),
                ('leapp-azure-sap-ha.repo', YUM_REPOS_PATH)
            ],
        },
        'azure-sap-apps': {
            'src_pkg': 'rhui-azure-rhel8-sapapps',
            'target_pkg': 'rhui-azure-rhel9-sapapps',
            'agent_pkg': 'WALinuxAgent',
            'leapp_pkg': 'leapp-rhui-azure-sap',
            'leapp_pkg_repo': 'leapp-azure-sap-apps.repo',
            'files_map': [
                ('content-sapapps.crt', RHUI_PKI_PRODUCT_DIR),
                ('content-eus.crt', RHUI_PKI_PRODUCT_DIR),
                ('key-sapapps.crt', RHUI_PKI_DIR),
                ('key-eus.crt', RHUI_PKI_DIR),
                ('leapp-azure-sap-apps.repo', YUM_REPOS_PATH)
            ],
        },
        'google': {
            'src_pkg': 'google-rhui-client-rhel8',
            'target_pkg': 'google-rhui-client-rhel9',
            'leapp_pkg': 'leapp-rhui-google',
            'leapp_pkg_repo': 'leapp-google.repo',
            'files_map': [
                ('content.crt', RHUI_PKI_PRODUCT_DIR),
                ('key.pem', RHUI_PKI_DIR),
                ('leapp-google.repo', YUM_REPOS_PATH)
            ],
        },
        'google-sap': {
            'src_pkg': 'google-rhui-client-rhel8-sap',
            'target_pkg': 'google-rhui-client-rhel9-sap',
            'leapp_pkg': 'leapp-rhui-google-sap',
            'leapp_pkg_repo': 'leapp-google-sap.repo',
            'files_map': [
                ('content.crt', RHUI_PKI_PRODUCT_DIR),
                ('key.pem', RHUI_PKI_DIR),
                ('leapp-google-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


def _read_rpm_modifications(config):
    """
    Ask RPM database whether the configuration file was modified.

    :param config: a config file to check
    """
    try:
        return stdlib.run(['rpm', '-Vf', config], split=True, checked=False)['stdout']
    except OSError as err:
        error = 'Failed to check the modification status of the file {}: {}'.format(config, str(err))
        stdlib.api.current_logger().error(error)
        return []


def _parse_config_modification(data, config):
    """
    Handle the output of rpm verify command to figure out if configuration file was modified.

    :param data: output of the rpm verify
    :param config: a config file to check
    """

    # 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 check_file_modification(config):
    """
    Check if the given configuration file tracked by RPM was modified

    This is useful when figuring out if the file will be replaced by the rpm on the upgrade
    or we need to take care of the upgrade manually.

    :param config: The configuration file to check
    """
    output = _read_rpm_modifications(config)
    return _parse_config_modification(output, config)


# repos/system_upgrade/common/libraries/systemd.py

import fnmatch
import os

from leapp.libraries.stdlib import api, CalledProcessError, run
from leapp.models import SystemdServiceFile, SystemdServicePreset

SYSTEMD_SYMLINKS_DIR = '/etc/systemd/system/'

_SYSTEMCTL_CMD_OPTIONS = ['--type=service', '--all', '--plain', '--no-legend']
_USR_PRESETS_PATH = '/usr/lib/systemd/system-preset/'
_ETC_PRESETS_PATH = '/etc/systemd/system-preset/'

SYSTEMD_SYSTEM_LOAD_PATH = [
    '/etc/systemd/system',
    '/usr/lib/systemd/system'
]


def get_broken_symlinks():
    """
    Get broken systemd symlinks on the system

    :return: List of broken systemd symlinks
    :rtype: list[str]
    :raises: CalledProcessError: if the `find` command fails
    :raises: OSError: if the find utility is not found
    """
    try:
        return run(['find', SYSTEMD_SYMLINKS_DIR, '-xtype', 'l'], split=True)['stdout']
    except (OSError, CalledProcessError):
        api.current_logger().error('Cannot obtain the list of broken systemd symlinks.')
        raise


def _try_call_unit_command(command, unit):
    try:
        # it is possible to call this on multiple units at once,
        # but failing to enable one service would cause others to not enable as well
        run(['systemctl', command, unit])
    except CalledProcessError as err:
        msg = 'Failed to {} systemd unit "{}". Message: {}'.format(command, unit, str(err))
        api.current_logger().error(msg)
        raise err


def enable_unit(unit):
    """
    Enable a systemd unit

    It is strongly recommended to produce SystemdServicesTasks message instead,
    unless it is absolutely necessary to handle failure yourself.

    :param unit: The systemd unit to enable
    :raises CalledProcessError: In case of failure
    """
    _try_call_unit_command('enable', unit)


def disable_unit(unit):
    """
    Disable a systemd unit

    It is strongly recommended to produce SystemdServicesTasks message instead,
    unless it is absolutely necessary to handle failure yourself.

    :param unit: The systemd unit to disable
    :raises CalledProcessError: In case of failure
    """
    _try_call_unit_command('disable', unit)


def reenable_unit(unit):
    """
    Re-enable a systemd unit

    It is strongly recommended to produce SystemdServicesTasks message, unless it
    is absolutely necessary to handle failure yourself.

    :param unit: The systemd unit to re-enable
    :raises CalledProcessError: In case of failure
    """
    _try_call_unit_command('reenable', unit)


def get_service_files():
    """
    Get list of unit files of systemd services on the system

    The list includes template units.

    :return: List of service unit files with states
    :rtype: list[SystemdServiceFile]
    :raises: CalledProcessError: in case of failure of `systemctl` command
    """
    services_files = []
    try:
        cmd = ['systemctl', 'list-unit-files'] + _SYSTEMCTL_CMD_OPTIONS
        service_units_data = run(cmd, split=True)['stdout']
    except CalledProcessError as err:
        api.current_logger().error('Cannot obtain the list of unit files:{}'.format(str(err)))
        raise

    for entry in service_units_data:
        columns = entry.split()
        services_files.append(SystemdServiceFile(name=columns[0], state=columns[1]))
    return services_files


def _join_presets_resolving_overrides(etc_files, usr_files):
    """
    Join presets and resolve preset file overrides

    Preset files in /etc/ override those with the same name in /usr/.
    If such a file is a symlink to /dev/null, it disables the one in /usr/ instead.

    :param etc_files: Systemd preset files in /etc/
    :param usr_files: Systemd preset files in /usr/
    :return: List of preset files in /etc/ and /usr/ with overridden files removed
    """
    for etc_file in etc_files:
        filename = os.path.basename(etc_file)
        for usr_file in usr_files:
            if filename == os.path.basename(usr_file):
                usr_files.remove(usr_file)
                if os.path.islink(etc_file) and os.readlink(etc_file) == '/dev/null':
                    etc_files.remove(etc_file)

    return etc_files + usr_files


def _search_preset_files(path):
    """
    Search preset files in the given path

    Presets are search recursively in the given directory.
    If path isn't an existing directory, return empty list.

    :param path: The path to search preset files in
    :return: List of found preset files
    :rtype: list[str]
    :raises: CalledProcessError: if the `find` command fails
    :raises: OSError: if the find utility is not found
    """
    if os.path.isdir(path):
        try:
            return run(['find', path, '-name', '*.preset'], split=True)['stdout']
        except (OSError, CalledProcessError) as err:
            api.current_logger().error('Cannot obtain list of systemd preset files in {}:{}'.format(path, str(err)))
            raise
    else:
        return []


def _get_system_preset_files():
    """
    Get systemd system preset files and remove overriding entries. Entries in /run/systemd/system are ignored.

    :return: List of system systemd preset files
    :raises: CalledProcessError: if the `find` command fails
    :raises: OSError: if the find utility is not found
    """
    etc_files = _search_preset_files(_ETC_PRESETS_PATH)
    usr_files = _search_preset_files(_USR_PRESETS_PATH)

    preset_files = _join_presets_resolving_overrides(etc_files, usr_files)
    preset_files.sort()
    return preset_files


def _recursive_glob(pattern, root_dir):
    for _, _, filenames in os.walk(root_dir):
        for filename in filenames:
            if fnmatch.fnmatch(filename, pattern):
                yield filename


def _parse_preset_entry(entry, presets, load_path):
    """
    Parse a single entry (line) in a preset file

    Single entry might set presets on multiple units using globs.

    :param entry: The entry to parse
    :param presets: Dictionary to store the presets into
    :param load_path: List of paths to look systemd unit files up in
    """

    columns = entry.split()
    if len(columns) < 2 or columns[0] not in ('enable', 'disable'):
        raise ValueError('Invalid preset file entry: "{}"'.format(entry))

    for path in load_path:
        # TODO(mmatuska): This currently also globs non unit files,
        # so the results need to be filtered with something like endswith('.<unit_type>')
        unit_files = _recursive_glob(columns[1], root_dir=path)

        for unit_file in unit_files:
            if '@' in columns[1] and len(columns) > 2:
                # unit is a template,
                # if the entry contains instance names after template unit name
                # the entry only applies to the specified instances, not to the
                # template itself
                for instance in columns[2:]:
                    service_name = unit_file[:unit_file.index('@') + 1] + instance + '.service'
                    if service_name not in presets:  # first occurrence has priority
                        presets[service_name] = columns[0]

            elif unit_file not in presets:  # first occurrence has priority
                presets[unit_file] = columns[0]


def _parse_preset_files(preset_files, load_path, ignore_invalid_entries):
    """
    Parse presets from preset files

    :param load_path: List of paths to search units at
    :param ignore_invalid_entries: Whether to ignore invalid entries in preset files or raise an error
    :return: Dictionary mapping systemd units to their preset state
    :rtype: dict[str, str]
    :raises: ValueError: when a preset file has invalid content
    """
    presets = {}

    for preset in preset_files:
        with open(preset, 'r') as preset_file:
            for line in preset_file:
                stripped = line.strip()
                if stripped and stripped[0] not in ('#', ';'):  # ignore comments
                    try:
                        _parse_preset_entry(stripped, presets, load_path)
                    except ValueError as err:
                        new_msg = 'Invalid preset file {pfile}: {error}'.format(pfile=preset, error=str(err))
                        if ignore_invalid_entries:
                            api.current_logger().warning(new_msg)
                            continue
                        raise ValueError(new_msg)
    return presets


def get_system_service_preset_files(service_files, ignore_invalid_entries=False):
    """
    Get system preset files for services

    Presets for static and transient services are filtered out.

    :param services_files: List of service unit files
    :param ignore_invalid_entries: Ignore invalid entries in preset files if True, raise ValueError otherwise
    :return: List of system systemd services presets
    :rtype: list[SystemdServicePreset]
    :raises: CalledProcessError: In case of errors when discovering systemd preset files
    :raises: OSError: When the `find` command is not available
    :raises: ValueError: When a preset file has invalid content and ignore_invalid_entries is False
    """
    preset_files = _get_system_preset_files()
    presets = _parse_preset_files(preset_files, SYSTEMD_SYSTEM_LOAD_PATH, ignore_invalid_entries)

    preset_models = []
    for unit, state in presets.items():
        if unit.endswith('.service'):
            service_file = next(iter([s for s in service_files if s.name == unit]), None)
            # presets can also be set on instances of template services which don't have a unit file
            if service_file and service_file.state in ('static', 'transient'):
                continue
            preset_models.append(SystemdServicePreset(service=unit, state=state))

    return preset_models


# repos/system_upgrade/common/libraries/testutils.py

import json
import logging
import os
from collections import namedtuple

from leapp import reporting
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.reports = []

    def __call__(self, report_fields):
        self.called += 1
        report_obj = reporting._create_report_object(report_fields)
        full_report = json.loads(report_obj.dump()['report'])
        self.reports.append(full_report)

    @property
    def report_fields(self):
        if self.reports:
            return self.reports[-1]
        return {}


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._common_tools_folder = '../../tools'
        self._actor_folder = 'files'
        self._actor_tools_folder = 'tools'
        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 get_common_tool_path(self, name):
        return os.path.join(self._common_tools_folder, name)

    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_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
from leapp.utils.deprecation import deprecated


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)


@deprecated(since='2022-02-03', message=(
        'The "apply_yum_workaround" function has been deprecated, use "DNFWorkaround" '
        'message as used in the successive "RegisterYumAdjustment" actor.'
    )
)
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 execute 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 losing 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 fields, Model
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 fields, Model
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/cephinfo.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class CephInfo(Model):
    topic = SystemInfoTopic

    encrypted_volumes = fields.List(fields.String(), default=[])


# repos/system_upgrade/common/models/cpuinfo.py

from leapp.models import fields, Model
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 needed, 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/devicedriverdeprecationdata.py

from leapp.models import fields, Model
from leapp.topics import SystemFactsTopic


class DeviceDriverDeprecationEntry(Model):
    """
    Describes a device or driver deprecated in RHEL
    """
    topic = SystemFactsTopic

    deprecation_announced = fields.String()
    """
    This field should contain a X.Y version in which version
    of RHEL the deprecation of the described driver or device was announced.

    NOTE: The value is not enforced to be in the X.Y format and might be empty.
          As not all data entries do contain this relevant information.
    """

    device_id = fields.String()
    """
    If this entry describes a device, this field will contain an identification
    string, relevant to the device.

    This identification string might contain sets of numbers {1,2,3},
    ranges [1-3] or even sets of ranges {[1-3],[5-9]} or a wildcard * which
    should match any number.

    Examples:
    - Range:            'x86_64:intel:6:[56-57]'
    - Set:              'x86_64:amd:23:{1,17,49}'
    - Set of Ranges:    'x86_64:amd:23:{[2-16],[18-48],[50-255]}'
    - Wildcard:         'x86_64:amd:21:*'
    """

    device_type = fields.StringEnum(choices=['pci', 'cpu'])
    """

    NOTE: Other devices might come later. The following values have been
    described:
    - 'usb'
    - 'video_adapter'
    - 'firewire'
    - 'qemu_machine'
    However they aren't in use yet. Since we wouldn't support it for
    now, we can extend it once we support it and just filter the ones supported,
    by leapp.
    """

    device_name = fields.String()
    """
    Human readable name of the device in question - Could be used to display.
    """

    driver_name = fields.String()
    """
    Name of the kernel driver for the device, or just a plain driver name.
    Might be an empty string.
    """

    available_in_rhel = fields.List(fields.Integer())
    """
    List of major version numbers, in which the device/driver can be found.
    If the target major version number is not in this list, the upgrade must
    be inhibited to avoid problems on boot.
    """
    maintained_in_rhel = fields.List(fields.Integer())
    """
    List of major version numbers, in which the device/driver is maintained. That
    means, that in those versions the device/driver actively gets patches and
    bug fixes.
    If the target major version number is not in this list, but is in the
    available_in_rhel list, a warning should be generated, but the upgrade must
    NOT be inhibited.
    """


class DetectedDeviceOrDriver(DeviceDriverDeprecationEntry):
    """
    DetectedDeviceOrDriver is used for reporting unsupported drivers or devices
    """


class DeviceDriverDeprecationData(Model):
    """
    Contains data related to deprecated devices and drivers in RHEL
    """
    topic = SystemFactsTopic

    entries = fields.List(fields.Model(DeviceDriverDeprecationEntry))
    """
    A list of entries describing deprecated devices and drivers
    """


# repos/system_upgrade/common/models/dnfplugintask.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class DNFPluginTask(Model):
    """
    Represents information what should DNF do with a specific 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/dnfworkaround.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class DNFWorkaround(Model):
    """
    DNFWorkaround is used to register scripts, that have to be executed to apply modifications to the system,
    just before DNF performs a transaction in order for it to succeed.

    As an example in the case of the RHEL7 to RHEL8 upgrade, we have to execute a script to fixup the way how
    yum and dnf symlinks are created as they cannot be replaced by RPM. To solve this we have created the
    handleyumconfig tool in the system_upgrade/el7toel8 repository and register the workaround with the
    registeryumadjustment actor.
    """
    topic = SystemInfoTopic

    script_path = fields.String()
    """
    Absolute path to a bash script to execute
    """

    script_args = fields.List(fields.String(), default=[])
    """
    Arguments with which the script should be executed

    In case that an argument contains a whitespace or an escapable character,
    the argument must be already treated correctly. e.g.
        `script_args = ['-i', 'my\\ string']
    """

    display_name = fields.String()
    """
    Name to display for this script when executed
    """


# repos/system_upgrade/common/models/firewallsfacts.py

from leapp.models import fields, Model
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 fields, Model
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 fields, Model
from leapp.topics import SystemFactsTopic


class GrubConfigError(Model):
    topic = SystemFactsTopic

    error_detected = fields.Boolean(default=False)
    error_type = fields.StringEnum(['GRUB_CMDLINE_LINUX syntax', 'missing newline'])


# repos/system_upgrade/common/models/grubdevice.py

from leapp.models import fields, Model
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 fields, Model
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 fields, Model
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 (canonical 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 (canonical 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 fields, Model
from leapp.topics import SystemFactsTopic


class InstalledDesktopsFacts(Model):
    """
    The model includes fact about installed
    """
    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 fields, Model
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 fields, Model
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 fields, Model
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 fields, Model
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 fields, Model
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 TargetKernelCmdlineArgTasks(Model):
    """
    Desired modifications of the target kernel args
    """
    topic = SystemInfoTopic

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


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 fields, Model, 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 fields, Model
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 identified 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/opensshconfig.py

from leapp.models import fields, Model
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 occurred 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. """
    deprecated_directives = fields.List(fields.String())
    """ Configuration directives that were deprecated in the new version of openssh. """
    subsystem_sftp = fields.Nullable(fields.String())
    """ The "Subsystem sftp" configuration option, if present """

    modified = fields.Boolean(default=False)
    """ True if the configuration file was modified. """


# repos/system_upgrade/common/models/packagemanagerinfo.py

from leapp.models import fields, Model
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.
    """

    configured_proxies = fields.List(fields.String(), default=[])
    """
    A sorted list of proxies present in yum and dnf configuration files.
    """

    enabled_plugins = fields.List(fields.String(), default=[])


# repos/system_upgrade/common/models/pcidevices.py

from leapp.models import fields, Model
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 fields, Model
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 fields, Model
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 fields, Model
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())
    proxy = 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 fields, Model
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', 'google'])
    """
    Specifies what cloud provider (RHUI) is the repository specific to.

    Empty string denotes that the repository is not specific to any cloud provider.
    """


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 fields, Model
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 fields, Model
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 fields, Model
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 fields, Model
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 InvalidRootSubdirectory(Model):
    """
    Representation of a single root subdirectory with non-utf name that is stored as a Blob.
    """
    topic = SystemFactsTopic
    name = fields.Blob()
    target = fields.Nullable(fields.Blob())


class RootDirectory(Model):
    topic = SystemFactsTopic
    items = fields.List(fields.Model(RootSubdirectory))  # should not be empty
    invalid_items = fields.Nullable(fields.List(fields.Model(InvalidRootSubdirectory)))


# 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 fields, Model
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 fields, Model
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 fields, Model
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 fields, Model
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 fields, Model
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()
    kname = fields.String()
    maj_min = fields.String()
    rm = fields.String()
    size = fields.String()
    bsize = fields.Integer()
    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 fields, Model
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/systemd.py

from leapp.models import fields, Model
from leapp.topics import SystemInfoTopic


class SystemdBrokenSymlinksSource(Model):
    """
    Information about broken systemd symlinks on the source system
    """

    topic = SystemInfoTopic
    broken_symlinks = fields.List(fields.String(), default=[])
    """
    List of broken systemd symlinks on the source system

    The values are absolute paths of the broken symlinks.
    """


class SystemdBrokenSymlinksTarget(SystemdBrokenSymlinksSource):
    """
    Analogy to :class:`SystemdBrokenSymlinksSource`, but for the target system
    """


class SystemdServicesTasks(Model):
    """
    Influence the systemd services of the target system

    E.g. it could be specified explicitly whether some services should
    be enabled or disabled after the in-place upgrade - follow descriptions
    of particular tasks for details.

    In case of conflicting tasks (e.g. the A service should be enabled and
    disabled in the same time):
       a) If conflicting tasks are detected during check phases,
          the upgrade is inhibited with the proper report.
       b) If conflicting tasks are detected during the final evaluation,
          error logs are created and such services will be disabled.
    """
    topic = SystemInfoTopic

    to_enable = fields.List(fields.String(), default=[])
    """
    List of systemd services to enable on the target system

    Masked services will not be enabled. Attempting to enable a masked service
    will be evaluated by systemctl as usually. The error will be logged and the
    upgrade process will continue.
    """

    to_disable = fields.List(fields.String(), default=[])
    """
    List of systemd services to disable on the target system
    """

    # NOTE: possible extension in case of requirement (currently not implemented):
    # to_unmask = fields.List(fields.String(), default=[])


class SystemdServiceFile(Model):
    """
    Information about single systemd service unit file

    This model is not expected to be produced nor consumed by actors directly.
    See the :class:`SystemdServicesInfoSource` and :class:`SystemdServicesPresetInfoTarget`
    for more info.
    """
    topic = SystemInfoTopic

    name = fields.String()
    """
    Name of the service unit file
    """

    state = fields.StringEnum([
        'alias',
        'bad',
        'disabled',
        'enabled',
        'enabled-runtime',
        'generated',
        'indirect',
        'linked',
        'linked-runtime',
        'masked',
        'masked-runtime',
        'static',
        'transient',
    ])
    """
    The state of the service unit file
    """


class SystemdServicesInfoSource(Model):
    """
    Information about systemd services on the source system
    """
    topic = SystemInfoTopic

    service_files = fields.List(fields.Model(SystemdServiceFile), default=[])
    """
    List of all installed systemd service unit files

    Instances of service template unit files don't have a unit file
    and therefore aren't included, but their template files are.
    Generated service unit files are also included.
    """


class SystemdServicesInfoTarget(SystemdServicesInfoSource):
    """
    Analogy to :class:`SystemdServicesInfoSource`, but for the target system

    This information is taken after the RPM Upgrade and might become
    invalid if there are actors calling systemctl enable/disable directly later
    in the upgrade process. Therefore it is recommended to use
    :class:`SystemdServicesTasks` to alter the state of units in the
    FinalizationPhase.
    """


class SystemdServicePreset(Model):
    """
    Information about a preset for systemd service
    """

    topic = SystemInfoTopic
    service = fields.String()
    """
    Name of the service, with the .service suffix
    """

    state = fields.StringEnum(['disable', 'enable'])
    """
    The state set by a preset file
    """


class SystemdServicesPresetInfoSource(Model):
    """
    Information about presets for systemd services
    """
    topic = SystemInfoTopic

    presets = fields.List(fields.Model(SystemdServicePreset), default=[])
    """
    List of all service presets
    """


class SystemdServicesPresetInfoTarget(SystemdServicesPresetInfoSource):
    """
    Analogy to :class:`SystemdServicesPresetInfoSource` but for the target system
    """


# repos/system_upgrade/common/models/targetrepositories.py

from leapp.models import fields, Model
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 fields, Model
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()
    """
    Canonical 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 fields, Model
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/upgradeiso.py

from leapp.models import CustomTargetRepository, fields, Model
from leapp.topics import SystemFactsTopic


class TargetOSInstallationImage(Model):
    """
    An installation image of a target OS requested to be the source of target OS packages.
    """
    topic = SystemFactsTopic
    path = fields.String()
    mountpoint = fields.String()
    repositories = fields.List(fields.Model(CustomTargetRepository))
    rhel_version = fields.String(default='')
    was_mounted_successfully = fields.Boolean(default=False)


# repos/system_upgrade/common/models/usedrepositories.py

from leapp.models import fields, Model
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 fields, Model
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 fields, Model
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/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 installed, 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.FailImmediately,
                            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 necessary 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/common/actors/addupgradebootentry/actor.py

import os

from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor.addupgradebootentry import add_boot_entry, fix_grub_config_error
from leapp.models import BootContent, FirmwareFacts, GrubConfigError, TargetKernelCmdlineArgTasks, TransactionDryRun
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag


class AddUpgradeBootEntry(Actor):
    """
    Add new boot entry for Leapp provided initramfs.

    Using new boot entry, Leapp can continue the upgrade process in the initramfs after reboot
    """

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

    def process(self):
        for grub_config_error in self.consume(GrubConfigError):
            if grub_config_error.error_detected:
                fix_grub_config_error('/etc/default/grub', grub_config_error.error_type)

        configs = None
        ff = next(self.consume(FirmwareFacts), None)
        if not ff:
            raise StopActorExecutionError(
                'Could not identify system firmware',
                details={'details': 'Actor did not receive FirmwareFacts message.'}
            )

        # related to issue with hybrid BIOS and UEFI images
        # https://bugzilla.redhat.com/show_bug.cgi?id=1667028
        if ff.firmware == 'bios' and os.path.ismount('/boot/efi') and os.path.isfile('/boot/efi/EFI/redhat/grub.cfg'):
            configs = ['/boot/grub2/grub.cfg', '/boot/efi/EFI/redhat/grub.cfg']
        add_boot_entry(configs)


# repos/system_upgrade/common/actors/applytransactionworkarounds/actor.py

from leapp.actors import Actor
from leapp.libraries.common import dnfplugin
from leapp.models import DNFWorkaround
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class ApplyTransactionWorkarounds(Actor):
    """
    Executes registered workaround scripts on the system before the upgrade transaction
    """

    name = 'applytransactionworkarounds'
    consumes = (DNFWorkaround,)
    produces = ()
    tags = (IPUWorkflowTag, PreparationPhaseTag)

    def process(self):
        dnfplugin.apply_workarounds()


# repos/system_upgrade/common/actors/baculacheck/actor.py

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


class BaculaCheck(Actor):
    """
    Actor checking for presence of Bacula installation.

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

    def process(self):
        report_installed_packages()


# repos/system_upgrade/common/actors/biosdevname/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.biosdevname import check_biosdevname
from leapp.models import KernelCmdlineArg, PersistentNetNamesFacts
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class Biosdevname(Actor):
    """
    Enable biosdevname on the target RHEL system if all interfaces on the source RHEL
    system use biosdevname naming scheme and if machine vendor is DELL
    """

    name = 'biosdevname'
    consumes = (PersistentNetNamesFacts,)
    produces = (KernelCmdlineArg,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        check_biosdevname()


# repos/system_upgrade/common/actors/cephvolumescan/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import cephvolumescan
from leapp.models import CephInfo, InstalledRPM
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CephVolumeScan(Actor):

    """
    Retrieves the list of encrypted Ceph OSD
    """

    name = 'cephvolumescan'
    consumes = (InstalledRPM,)
    produces = (CephInfo,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        output = cephvolumescan.encrypted_osds_list()
        self.produce(CephInfo(encrypted_volumes=output))


# repos/system_upgrade/common/actors/checkbootavailspace/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkbootavailspace import (
    check_avail_space_on_boot,
    get_avail_bytes_on_boot,
)
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckBootAvailSpace(Actor):
    """
    Check if at least 100Mib of available space on /boot. If not, inhibit the upgrade process.

    Rationale for the requirement of 100MiB:
    - Before reboot into initramfs, the CopyInitramfsToBoot actor copies kernel and initramfs to
      /boot, together worth of 66MiB.
    - After booting into initramfs, the RemoveBootFiles actor removes the copied kernel and
      initramfs from /boot.
    - The DnfShellRpmUpgrade installs a new kernel-core package which puts additional 54MiB of data
      to /boot.
    - Even though the available space needed at the time of writing this actor is 66MiB, the
      additional 100-66=34MiB is a leeway for potential growth of the kernel or initramfs in size.
    """

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

    def process(self):
        check_avail_space_on_boot(get_avail_bytes_on_boot)


# repos/system_upgrade/common/actors/checkdetecteddevicesanddrivers/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkdddd
from leapp.models import DetectedDeviceOrDriver, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckDetectedDevicesAndDrivers(Actor):
    """
    Checks whether or not detected devices and drivers are usable on the target system.

    In case a driver is no longer present in the target system, an inhibitor will be raised.
    If the device or driver is not maintained anymore, a warning report will be generated.
    """

    name = 'check_detected_devices_and_drivers'
    consumes = (DetectedDeviceOrDriver,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        checkdddd.process()


# repos/system_upgrade/common/actors/checketcreleasever/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checketcreleasever
from leapp.models import PkgManagerInfo, Report, RHUIInfo
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckEtcReleasever(Actor):
    """
    Check releasever info and provide a guidance based on the facts
    """

    name = 'check_etc_releasever'
    consumes = (PkgManagerInfo, RHUIInfo)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checketcreleasever.process()


# repos/system_upgrade/common/actors/checkfips/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.models import KernelCmdline, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckFips(Actor):
    """
    Inhibit upgrade if FIPS is detected as enabled.
    """

    name = 'check_fips'
    consumes = (KernelCmdline,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        cmdline = next(self.consume(KernelCmdline), None)
        if not cmdline:
            raise StopActorExecutionError('Cannot check FIPS state due to missing command line parameters',
                                          details={'Problem': 'Did not receive a message with kernel command '
                                                              'line parameters (KernelCmdline)'})
        for parameter in cmdline.parameters:
            if parameter.key == 'fips' and parameter.value == '1':
                title = 'Cannot upgrade a system with FIPS mode enabled'
                summary = 'Leapp has detected that FIPS is enabled on this system. ' \
                          'In-place upgrade of systems in FIPS mode is currently unsupported.'
                reporting.create_report([
                    reporting.Title(title),
                    reporting.Summary(summary),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Groups([reporting.Groups.SECURITY]),
                    reporting.Groups([reporting.Groups.INHIBITOR])
                ])


# repos/system_upgrade/common/actors/checkgrubcore/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.common.config import architecture
from leapp.models import FirmwareFacts, GrubDevice, UpdateGrub
from leapp.reporting import create_report, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
from leapp.utils.deprecation import suppress_deprecation

GRUB_SUMMARY = ('On legacy (BIOS) systems, GRUB core (located in the gap between the MBR and the '
                'first partition) does not get automatically updated when GRUB is upgraded.')


# TODO: remove this actor completely after the deprecation period expires
@suppress_deprecation(GrubDevice, UpdateGrub)
class CheckGrubCore(Actor):
    """
    Check whether we are on legacy (BIOS) system and instruct Leapp to upgrade GRUB core
    """

    name = 'check_grub_core'
    consumes = (FirmwareFacts, GrubDevice)
    produces = (Report, UpdateGrub)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        if architecture.matches_architecture(architecture.ARCH_S390X):
            # s390x archs use ZIPL instead of GRUB
            return

        ff = next(self.consume(FirmwareFacts), None)
        if ff and ff.firmware == 'bios':
            dev = next(self.consume(GrubDevice), None)
            if dev:
                self.produce(UpdateGrub(grub_device=dev.grub_device))
                create_report([
                    reporting.Title(
                        'GRUB core will be updated during upgrade'
                    ),
                    reporting.Summary(GRUB_SUMMARY),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Groups([reporting.Groups.BOOT]),
                ])
            else:
                create_report([
                    reporting.Title('Leapp could not identify where GRUB core is located'),
                    reporting.Summary(
                        'We assume GRUB core is located on the same device as /boot. Leapp needs to '
                        'update GRUB core as it is not done automatically on legacy (BIOS) systems. '
                    ),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Groups([reporting.Groups.BOOT]),
                    reporting.Remediation(
                        hint='Please run "grub2-install <GRUB_DEVICE> command manually after upgrade'),
                ])


# repos/system_upgrade/common/actors/checkipaserver/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkipaserver import ipa_inhibit_upgrade, ipa_warn_pkg_installed
from leapp.models import IpaInfo
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckIPAServer(Actor):
    """
    Check for ipa-server and inhibit upgrade
    """

    name = "check_ipa_server"
    consumes = (IpaInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        for ipainfo in self.consume(IpaInfo):
            if ipainfo.is_server_configured:
                self.log.info(
                    "IdM server instance detected, inhibit upgrade"
                )
                ipa_inhibit_upgrade(ipainfo)
            elif ipainfo.has_server_package:
                self.log.info("Unused ipa-server package detected")
                ipa_warn_pkg_installed(ipainfo)


# repos/system_upgrade/common/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/common/actors/checkmountoptions/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkmountoptions import check_mount_options
from leapp.models import StorageInfo
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckMountOptions(Actor):
    """
    Check for mount options preventing the upgrade.

    Checks performed:
        - /var is mounted with the noexec option
    """
    name = "check_mount_options"
    consumes = (StorageInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag,)

    def process(self):
        check_mount_options()


# repos/system_upgrade/common/actors/checknfs/actor.py

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


class CheckNfs(Actor):
    """
    Check if NFS filesystem is in use. If yes, inhibit the upgrade process.

    Actor looks for NFS in the following sources: /ets/fstab, mount and systemd-mount.
    If there is NFS in any of the mentioned sources, actors inhibits the upgrade.
    """
    name = "check_nfs"
    consumes = (StorageInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag,)

    def process(self):
        details = "NFS is currently not supported by the inplace upgrade.\n" \
                  "We have found NFS usage at the following locations:\n"

        def _is_nfs(a_type):
            return a_type.startswith('nfs') and a_type != 'nfsd'

        for storage in self.consume(StorageInfo):
            # Check fstab
            fstab_nfs_mounts = []
            for fstab in storage.fstab:
                if _is_nfs(fstab.fs_vfstype):
                    fstab_nfs_mounts.append(" - {} {}\n".format(fstab.fs_spec, fstab.fs_file))

            # Check mount
            nfs_mounts = []
            for mount in storage.mount:
                if _is_nfs(mount.tp):
                    nfs_mounts.append(" - {} {}\n".format(mount.name, mount.mount))

            # Check systemd-mount
            systemd_nfs_mounts = []
            for systemdmount in storage.systemdmount:
                if _is_nfs(systemdmount.fs_type):
                    # mountpoint is not available in the model
                    systemd_nfs_mounts.append(" - {}\n".format(systemdmount.node))

        if any((fstab_nfs_mounts, nfs_mounts, systemd_nfs_mounts)):
            if fstab_nfs_mounts:
                details += "- NFS shares found in /etc/fstab:\n"
                details += ''.join(fstab_nfs_mounts)

            if nfs_mounts:
                details += "- NFS shares currently mounted:\n"
                details += ''.join(nfs_mounts)

            if systemd_nfs_mounts:
                details += "- NFS mounts configured with systemd-mount:\n"
                details += ''.join(systemd_nfs_mounts)

            fstab_related_resource = [reporting.RelatedResource('file', '/etc/fstab')] if fstab_nfs_mounts else []

            create_report([
                reporting.Title("Use of NFS detected. Upgrade can't proceed"),
                reporting.Summary(details),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups([
                        reporting.Groups.FILESYSTEM,
                        reporting.Groups.NETWORK
                ]),
                reporting.Remediation(hint='Disable NFS temporarily for the upgrade if possible.'),
                reporting.Groups([reporting.Groups.INHIBITOR]),
                ] + fstab_related_resource
            )


# repos/system_upgrade/common/actors/checknonmountboots390/actor.py

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


class CheckNonMountBootS390(Actor):
    """
    Inhibits on s390 when /boot is NOT on a separate partition.

    Due to some problems, if /boot is not on a separate partition, leapp is deleting the content of /boot.
    To avoid this from happening, we are inhibiting the upgrade process until this problem has been solved.
    """

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

    def process(self):
        checknonmountboots390.perform_check()


# repos/system_upgrade/common/actors/checkosrelease/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkosrelease import check_os_version, skip_check
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckOSRelease(Actor):
    """
    Check if the current RHEL minor version is supported. If not, inhibit the upgrade process.

    This check can be skipped by using the LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE environment variable.
    """

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

    def process(self):
        if not skip_check():
            check_os_version()


# repos/system_upgrade/common/actors/checkpersistentmounts/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkpersistentmounts import check_persistent_mounts
from leapp.models import StorageInfo
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckPersistentMounts(Actor):
    """
    Check if mounts required to be persistent are mounted in persistent fashion.

    Checks performed:
        - if /var/lib/leapp is mounted it has an entry in /etc/fstab
    """
    name = "check_persistent_mounts"
    consumes = (StorageInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag,)

    def process(self):
        check_persistent_mounts()


# repos/system_upgrade/common/actors/checkrhsmsku/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkrhsmsku
from leapp.models import Report, RHSMInfo
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckRedHatSubscriptionManagerSKU(Actor):
    """
    Ensure the system is subscribed to the subscription manager

    This actor verifies that the system is correctly subscribed to via the Red Hat Subscription Manager and
    has attached SKUs. The actor will inhibit the upgrade if there are none and RHSM is not supposed
    to be skipped.
    """

    name = 'check_rhsmsku'
    consumes = (RHSMInfo,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        checkrhsmsku.process()


# repos/system_upgrade/common/actors/checkrootsymlinks/actor.py

import os

from leapp import reporting
from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.models import Report, RootDirectory
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckRootSymlinks(Actor):
    """
    Check if the symlinks /bin and /lib are relative, not absolute.

    After reboot, dracut fails if the links are absolute.
    """

    name = 'check_root_symlinks'
    consumes = (RootDirectory,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        rootdir = next(self.consume(RootDirectory), None)
        if not rootdir:
            raise StopActorExecutionError('Cannot check root symlinks',
                                          details={'Problem': 'Did not receive a message with '
                                                              'root subdirectories'})
        absolute_links = [item for item in rootdir.items if item.target and os.path.isabs(item.target)]
        absolute_links_nonutf = [item for item in rootdir.invalid_items if item.target and os.path.isabs(item.target)]
        if not absolute_links and not absolute_links_nonutf:
            return

        report_fields = [
                reporting.Title('Upgrade requires links in root directory to be relative'),
                reporting.Summary(
                    'After rebooting, parts of the upgrade process can fail if symbolic links in / '
                    'point to absolute paths.\n'
                    'Please change these links to relative ones.'
                    ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups([reporting.Groups.INHIBITOR])]

        # Generate reports about absolute links presence
        rem_commands = []
        if absolute_links:
            commands = []
            for item in absolute_links:
                command = ' '.join(['ln',
                                    '-snf',
                                    os.path.relpath(item.target, '/'),
                                    os.path.join('/', item.name)])
                commands.append(command)
            rem_commands = [['sh', '-c', ' && '.join(commands)]]
        # Generate reports about non-utf8 absolute links presence
        nonutf_count = len(absolute_links_nonutf)
        if nonutf_count > 0:
            # for non-utf encoded filenames can't provide a remediation command, so will mention this fact in a hint
            rem_hint = ("{} symbolic links point to absolute paths that have non-utf8 encoding and need to be"
                        " fixed additionally".format(nonutf_count))
            report_fields.append(reporting.Remediation(hint=rem_hint, commands=rem_commands))
        else:
            report_fields.append(reporting.Remediation(commands=rem_commands))

        reporting.create_report(report_fields)


# repos/system_upgrade/common/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/common/actors/checkselinux/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checkselinux
from leapp.models import KernelCmdlineArg, Report, SELinuxFacts, SelinuxPermissiveDecision, SelinuxRelabelDecision
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckSelinux(Actor):
    """
    Check SELinux status and produce decision messages for further action.

    Based on SELinux status produces decision messages to relabeling and changing status if
    necessary
    """

    name = 'check_se_linux'
    consumes = (SELinuxFacts,)
    produces = (
        KernelCmdlineArg,
        Report,
        SelinuxPermissiveDecision,
        SelinuxRelabelDecision,
    )
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        checkselinux.process()


# repos/system_upgrade/common/actors/checkskippedrepositories/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.stdlib import config
from leapp.models import Report, SkippedRepositories
from leapp.reporting import create_report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckSkippedRepositories(Actor):
    """
    Produces a report if any repositories enabled on the system are going to be skipped.

    The report produced by this actor should additionally include any package that is affected due to skipping
    the repository.
    """

    name = 'check_skipped_repositories'
    consumes = (SkippedRepositories,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        repos = set()
        packages = set()

        for message in self.consume(SkippedRepositories):
            repos.update(message.repos)
            packages.update(message.packages)

        if repos:
            title = 'Some enabled RPM repositories are unknown to Leapp'
            summary_data = []
            summary_data.append('The following repositories with Red Hat-signed packages are unknown to Leapp:')
            summary_data.extend(['- {}'.format(r) for r in repos])
            summary_data.append('And the following packages installed from those repositories may not be upgraded:')
            summary_data.extend(['- {}'.format(p) for p in packages])
            summary = '\n'.join(summary_data)

            packages_related = [reporting.RelatedResource('package', str(p)) for p in packages]
            repos_related = [reporting.RelatedResource('repository', str(r)) for r in repos]

            create_report([
                reporting.Title(title),
                reporting.Summary(summary),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Groups([reporting.Groups.REPOSITORY]),
                reporting.Remediation(
                    hint='You can file a request to add this repository to the scope of in-place upgrades '
                         'by filing a support ticket')
            ] + packages_related + repos_related)

            if config.is_verbose():
                self.log.info('\n'.join([title, summary]))


# repos/system_upgrade/common/actors/checkskipphase/actor.py

from leapp.actors import Actor
from leapp.messaging.commands import SkipPhasesUntilCommand
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckSkipPhase(Actor):
    """
    Skip all the subsequent phases until the report phase.

    The phases that follow after the Checks phase work with the target
    RHEL major version user space - stuff around preparing and checking
    the rpm transaction. We do not want to process those phases in case of
    inhibition - e.g. for a specific HW unsupported by the target system we
    cannot do anything - we can just see some unclear errors in such case.
    So we want to instead skip to the Reports phase to provide clear report
    to user without confusing errors.

    The actor is processed after all actors in the phase (that provides Report
    messages) are processed.
    """

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

    def process(self):
        results = list(self.consume(Report))
        inhibitors = [msg for msg in results if 'inhibitor' in msg.report.get('flags', [])]
        if inhibitors:
            self.log.info("An upgrade inhibitor detected. Skipping to the Report phase.")
            # until_phase='targettransactioncheck'  === phase after this phase will be processed
            # === the Reports phase
            self._messaging.command(SkipPhasesUntilCommand(until_phase='targettransactioncheck'))


# repos/system_upgrade/common/actors/checksystemarch/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checksystemarch
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckSystemArch(Actor):
    """
    Check if system is running at a supported architecture. If no, inhibit the upgrade process.

    Base on collected system facts, verify if current architecture is supported, otherwise produces
    a message to inhibit upgrade process
    """

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

    def process(self):
        checksystemarch.check_architecture()


# repos/system_upgrade/common/actors/checktargetiso/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import check_target_iso
from leapp.models import Report, StorageInfo, TargetOSInstallationImage
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckTargetISO(Actor):
    """
    Check that the provided target ISO is a valid ISO image and is located on a persistent partition.
    """

    name = 'check_target_iso'
    consumes = (StorageInfo, TargetOSInstallationImage,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        check_target_iso.perform_target_iso_checks()


# repos/system_upgrade/common/actors/checktargetrepos/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import checktargetrepos
from leapp.models import CustomTargetRepositoryFile, Report, RHUIInfo, TargetRepositories
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class Checktargetrepos(Actor):
    """
    Check whether target yum repositories are specified.

    RHSM | RHUI | ER | CTR | CTRF || result
    -----+------+----+-----+------++-------
     Yes | ---- | -- | --- | ---- || -
    -----+------+----+-----+------++-------
     No  | Yes  | -- | --- | ---- || -
    -----+------+----+-----+------++-------
     No  | No   | -- | No  | No   || inhibit
    -----+------+----+-----+------++-------
     No  | No   | -- | No  | Yes  || inhibit
    -----+------+----+-----+------++-------
     No  | No   | No | Yes | No   || warn/report info
    -----+------+----+-----+------++-------
     No  | No   | No | Yes | Yes  || -
    -----+------+----+-----+------++-------
     No  | No   | Yes| Yes | No   || -
    -----+------+----+-----+------++-------
     No  | No   | Yes| Yes | Yes  || -

       ER   - config.get_env('LEAPP_ENABLE_REPOS') is non-empty
       CTR  - CustomTargetRepositories found
       CTRF - the expected CustomTargetRepositoryFile found
       RHSM - RHSM is used (it is not skipped)
       RHUI - RHUIInfo msg is consumed

    This is not 100 % reliable check. This cover just the most obvious cases
    that are expected to fail. Reporting of such issues in this way, here,
    will be probably much more clear, without additional errors that could
    be raised.
    """

    name = 'checktargetrepos'
    consumes = (CustomTargetRepositoryFile, TargetRepositories, RHUIInfo)
    produces = (Report)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        checktargetrepos.process()


# repos/system_upgrade/common/actors/checkyumpluginsenabled/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.checkyumpluginsenabled import check_required_yum_plugins_enabled
from leapp.models import PkgManagerInfo
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class CheckYumPluginsEnabled(Actor):
    """
    Checks that the required yum plugins are enabled.
    """

    name = 'check_yum_plugins_enabled'
    consumes = (PkgManagerInfo,)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        pkg_manager_info = next(self.consume(PkgManagerInfo))
        check_required_yum_plugins_enabled(pkg_manager_info)


# repos/system_upgrade/common/actors/commonleappdracutmodules/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import modscan
from leapp.tags import FactsPhaseTag, IPUWorkflowTag
from leapp.utils.deprecation import suppress_deprecation

from leapp.models import (  # isort:skip
    RequiredUpgradeInitramPackages,  # deprecated
    UpgradeDracutModule,  # deprecated
    TargetUserSpaceUpgradeTasks,
    UpgradeInitramfsTasks
)


@suppress_deprecation(RequiredUpgradeInitramPackages, UpgradeDracutModule)
class CommonLeappDracutModules(Actor):
    """
    Influences the generation of the initram disk

    The initram disk generation is influenced by specifying necessary dracut modules and packages that are
    required to be installed in the target userspace so required fields can be included.
    Modules to be added are specified via the UpgradeDracutModule message.
    Packages to install on the target userspace are specified by the RequiredUpgradeInitramPackages message.
    """

    name = 'common_leapp_dracut_modules'
    consumes = ()
    produces = (
        RequiredUpgradeInitramPackages,  # deprecated
        TargetUserSpaceUpgradeTasks,
        UpgradeDracutModule,  # deprecated
        UpgradeInitramfsTasks,
    )
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        modscan.process()


# repos/system_upgrade/common/actors/createisorepofile/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import create_iso_repofile
from leapp.models import CustomTargetRepositoryFile, TargetOSInstallationImage
from leapp.tags import IPUWorkflowTag, TargetTransactionFactsPhaseTag


class CreateISORepofile(Actor):
    """
    Create custom repofile containing information about repositories found in target OS installation ISO, if used.
    """

    name = 'create_iso_repofile'
    consumes = (TargetOSInstallationImage,)
    produces = (CustomTargetRepositoryFile,)
    tags = (IPUWorkflowTag, TargetTransactionFactsPhaseTag)

    def process(self):
        create_iso_repofile.produce_repofile_if_iso_used()


# repos/system_upgrade/common/actors/createresumeservice/actor.py

import os
import shutil

from leapp import reporting
from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.stdlib import api
from leapp.reporting import create_report, Report
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class CreateSystemdResumeService(Actor):
    """
    Add a systemd service to launch Leapp.

    Create a systemd service which will resume Leapp upgrade after the first reboot.
    """

    name = 'create_systemd_service'
    consumes = ()
    produces = (Report,)
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        service_name = 'leapp_resume.service'
        systemd_dir = '/etc/systemd/system'

        service_templ_fpath = self.get_file_path(service_name)
        shutil.copyfile(service_templ_fpath, os.path.join(systemd_dir, service_name))

        service_path = '/etc/systemd/system/{}'.format(service_name)
        symlink_path = '/etc/systemd/system/default.target.wants/{}'.format(service_name)

        # in case nothing is enabled in the default target, the directory does not exist
        try:
            os.mkdir(os.path.join(systemd_dir, 'default.target.wants'))
        except OSError:
            pass

        if os.path.exists(symlink_path):
            api.current_logger().debug(
                'Symlink {} already exists (from previous upgrade?). Removing... '.format(symlink_path)
            )
            os.unlink(symlink_path)

        try:
            os.symlink(service_path, symlink_path)
        except OSError as e:
            raise StopActorExecutionError(
                    'Could not create a symlink to enable {}'.format(service_name),
                    details={"details": str(e)})

        create_report([
            reporting.Title('Leapp resume systemd service enabled'),
            reporting.Summary(
                '{} enabled as oneshot systemd service to resume Leapp execution '
                'after reboot.'.format(service_name)
            ),
            reporting.Severity(reporting.Severity.INFO),
            reporting.Groups([reporting.Groups.UPGRADE_PROCESS]),
            reporting.RelatedResource('file', service_path),
            reporting.RelatedResource('file', symlink_path),
            reporting.RelatedResource('service', service_name)
        ])


# repos/system_upgrade/common/actors/detectkerneldrivers/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import detectkerneldrivers
from leapp.models import ActiveKernelModulesFacts, DetectedDeviceOrDriver, DeviceDriverDeprecationData
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class DetectKernelDrivers(Actor):
    """
    Matches all currently loaded kernel drivers against known deprecated and removed drivers.
    """

    name = 'detect_kernel_drivers'
    consumes = (DeviceDriverDeprecationData, ActiveKernelModulesFacts)
    produces = (DetectedDeviceOrDriver,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        detectkerneldrivers.process()


# repos/system_upgrade/common/actors/detectmissingnewlineingrubcfg/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.actor.detectmissingnewlineingrubcfg import is_grub_config_missing_final_newline
from leapp.models import GrubConfigError
from leapp.reporting import create_report, Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class DetectMissingNewlineInGrubCfg(Actor):
    """
    Check the grub configuration for a missing newline at its end.
    """

    name = 'detect_missing_newline_in_grub_cfg'
    consumes = ()
    produces = (Report, GrubConfigError)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        config = '/etc/default/grub'
        if is_grub_config_missing_final_newline(config):
            create_report([
                reporting.Title('Detected a missing newline at the end of grub configuration file.'),
                reporting.Summary(
                    'The missing newline in /etc/default/grub causes booting issues when appending '
                    'new entries to this file during the upgrade. Leapp will automatically fix this '
                    'problem by appending the missing newline to the grub configuration file.'
                ),
                reporting.Severity(reporting.Severity.LOW),
                reporting.Groups([reporting.Groups.BOOT]),
                reporting.RelatedResource('file', config)
            ])

            config_error = GrubConfigError(error_detected=True, error_type='missing newline')
            self.produce(config_error)


# repos/system_upgrade/common/actors/dnfdryrun/actor.py

from leapp.actors import Actor
from leapp.libraries.common import dnfplugin
from leapp.models import (
    BootContent,
    DNFPluginTask,
    DNFWorkaround,
    FilteredRpmTransactionTasks,
    RHUIInfo,
    StorageInfo,
    TargetOSInstallationImage,
    TargetUserSpaceInfo,
    TransactionDryRun,
    UsedTargetRepositories,
    XFSPresence
)
from leapp.tags import InterimPreparationPhaseTag, IPUWorkflowTag


class DnfDryRun(Actor):
    """
    Actor that invokes DNF to download the RPMs required for the upgrade transaction.

    This actor uses the rhel-upgrade plugin to perform the download of RPM for the transaction and performing the
    transaction test, that is something like a dry run trying to determine the success of the upgrade.
    """

    name = 'dnf_dry_run'
    consumes = (
        BootContent,
        DNFPluginTask,
        DNFWorkaround,
        FilteredRpmTransactionTasks,
        RHUIInfo,
        StorageInfo,
        TargetOSInstallationImage,
        TargetUserSpaceInfo,
        UsedTargetRepositories,
        XFSPresence,
    )
    produces = (TransactionDryRun,)
    tags = (IPUWorkflowTag, InterimPreparationPhaseTag)

    def process(self):
        xfs_info = next(self.consume(XFSPresence), XFSPresence())
        storage_info = next(self.consume(StorageInfo), StorageInfo())
        used_repos = self.consume(UsedTargetRepositories)
        plugin_info = list(self.consume(DNFPluginTask))
        tasks = next(self.consume(FilteredRpmTransactionTasks), FilteredRpmTransactionTasks())
        target_userspace_info = next(self.consume(TargetUserSpaceInfo), None)
        rhui_info = next(self.consume(RHUIInfo), None)
        target_iso = next(self.consume(TargetOSInstallationImage), None)
        on_aws = bool(rhui_info and rhui_info.provider == 'aws')

        dnfplugin.perform_dry_run(
            tasks=tasks, used_repos=used_repos, target_userspace_info=target_userspace_info,
            xfs_info=xfs_info, storage_info=storage_info, plugin_info=plugin_info, on_aws=on_aws,
            target_iso=target_iso,
        )
        self.produce(TransactionDryRun())


# repos/system_upgrade/common/actors/dnfpackagedownload/actor.py

from leapp.actors import Actor
from leapp.libraries.common import dnfplugin
from leapp.models import (
    DNFPluginTask,
    DNFWorkaround,
    FilteredRpmTransactionTasks,
    RHUIInfo,
    StorageInfo,
    TargetOSInstallationImage,
    TargetUserSpaceInfo,
    UsedTargetRepositories,
    XFSPresence
)
from leapp.tags import DownloadPhaseTag, IPUWorkflowTag


class DnfPackageDownload(Actor):
    """
    Actor that invokes DNF to download the RPMs required for the upgrade transaction.

    This actor uses the rhel-upgrade plugin to perform the download of RPM for the transaction and performing the
    transaction test, that is something like a dry run trying to determine the success of the upgrade.
    """

    name = 'dnf_package_download'
    consumes = (
        DNFPluginTask,
        DNFWorkaround,
        FilteredRpmTransactionTasks,
        RHUIInfo,
        StorageInfo,
        TargetOSInstallationImage,
        TargetUserSpaceInfo,
        UsedTargetRepositories,
        XFSPresence,
    )
    produces = ()
    tags = (IPUWorkflowTag, DownloadPhaseTag)

    def process(self):
        xfs_info = next(self.consume(XFSPresence), XFSPresence())
        storage_info = next(self.consume(StorageInfo), StorageInfo())
        used_repos = self.consume(UsedTargetRepositories)
        plugin_info = list(self.consume(DNFPluginTask))
        tasks = next(self.consume(FilteredRpmTransactionTasks), FilteredRpmTransactionTasks())
        target_userspace_info = next(self.consume(TargetUserSpaceInfo), None)
        rhui_info = next(self.consume(RHUIInfo), None)
        # there are several "variants" related to the *AWS* provider (aws, aws-sap)
        on_aws = bool(rhui_info and rhui_info.provider.startswith('aws'))
        target_iso = next(self.consume(TargetOSInstallationImage), None)

        dnfplugin.perform_rpm_download(
            tasks=tasks, used_repos=used_repos, target_userspace_info=target_userspace_info,
            xfs_info=xfs_info, storage_info=storage_info, plugin_info=plugin_info, on_aws=on_aws,
            target_iso=target_iso
        )


# repos/system_upgrade/common/actors/dnftransactioncheck/actor.py

from leapp.actors import Actor
from leapp.libraries.common import dnfplugin
from leapp.models import (
    DNFPluginTask,
    DNFWorkaround,
    FilteredRpmTransactionTasks,
    StorageInfo,
    TargetOSInstallationImage,
    TargetUserSpaceInfo,
    UsedTargetRepositories,
    XFSPresence
)
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag


class DnfTransactionCheck(Actor):
    """
    This actor tries to solve the RPM transaction to verify the all package dependencies can be successfully resolved.
    """

    name = 'dnf_transaction_check'
    consumes = (
        DNFPluginTask,
        DNFWorkaround,
        FilteredRpmTransactionTasks,
        StorageInfo,
        TargetOSInstallationImage,
        TargetUserSpaceInfo,
        UsedTargetRepositories,
        XFSPresence,
    )
    produces = ()
    tags = (IPUWorkflowTag, TargetTransactionChecksPhaseTag)

    def process(self):
        xfs_info = next(self.consume(XFSPresence), XFSPresence())
        storage_info = next(self.consume(StorageInfo), StorageInfo())
        used_repos = self.consume(UsedTargetRepositories)
        plugin_info = list(self.consume(DNFPluginTask))
        tasks = next(self.consume(FilteredRpmTransactionTasks), FilteredRpmTransactionTasks())
        target_userspace_info = next(self.consume(TargetUserSpaceInfo), None)
        target_iso = next(self.consume(TargetOSInstallationImage), None)

        if target_userspace_info:
            dnfplugin.perform_transaction_check(
                tasks=tasks, used_repos=used_repos, target_userspace_info=target_userspace_info,
                xfs_info=xfs_info, storage_info=storage_info, plugin_info=plugin_info, target_iso=target_iso
            )


# repos/system_upgrade/common/actors/dnfupgradetransaction/actor.py

import shutil

from leapp.actors import Actor
from leapp.libraries.common import dnfplugin
from leapp.libraries.stdlib import run
from leapp.models import (
    DNFPluginTask,
    DNFWorkaround,
    FilteredRpmTransactionTasks,
    RHSMInfo,
    StorageInfo,
    TargetUserSpaceInfo,
    TransactionCompleted,
    UsedTargetRepositories
)
from leapp.tags import IPUWorkflowTag, RPMUpgradePhaseTag


class DnfUpgradeTransaction(Actor):
    """
    Setup and call DNF upgrade command

    Based on previously calculated RPM transaction data, this actor will setup and call
    rhel-upgrade DNF plugin with necessary parameters
    """

    name = 'dnf_upgrade_transaction'
    consumes = (
        DNFPluginTask,
        DNFWorkaround,
        FilteredRpmTransactionTasks,
        RHSMInfo,
        StorageInfo,
        TargetUserSpaceInfo,
        UsedTargetRepositories,
    )
    produces = (TransactionCompleted,)
    tags = (RPMUpgradePhaseTag, IPUWorkflowTag)

    def process(self):
        src_rhsm_info = next(self.consume(RHSMInfo), None)
        if src_rhsm_info:
            for prod_cert in src_rhsm_info.existing_product_certificates:
                run(['rm', '-f', prod_cert])

        used_repos = self.consume(UsedTargetRepositories)
        storage_info = next(self.consume(StorageInfo), None)
        plugin_info = list(self.consume(DNFPluginTask))
        tasks = next(self.consume(FilteredRpmTransactionTasks), FilteredRpmTransactionTasks())
        target_userspace_info = next(self.consume(TargetUserSpaceInfo), None)

        dnfplugin.perform_transaction_install(
            tasks=tasks, used_repos=used_repos, storage_info=storage_info, target_userspace_info=target_userspace_info,
            plugin_info=plugin_info
        )
        self.produce(TransactionCompleted())
        userspace = next(self.consume(TargetUserSpaceInfo), None)
        if userspace:
            try:
                shutil.rmtree(userspace.path)
            except EnvironmentError:
                self.log.info("Failed to remove temporary userspace - error ignored", exc_info=True)


# repos/system_upgrade/common/actors/enablerhsmtargetrepos/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import enablerhsmtargetrepos
from leapp.models import UsedTargetRepositories
from leapp.tags import FirstBootPhaseTag, IPUWorkflowTag


class EnableRHSMTargetRepos(Actor):
    """
    On the upgraded target system, set release and enable repositories that were used during the upgrade
    transaction.

    We are enabling those RHEL target repos that are equivalent to the enabled source RHEL ones available.
    The BaseOS and AppStream repos are enabled on the target RHEL by default. Any other repository needs to
    be enabled specifically using the subscription-manager (RHSM) utility. In case some custom repo was used
    during the upgrade transaction, it won't be enabled by this actor as it is unknown to the subscription-manager.

    We need to overwrite any RHSM release that may have been set before the upgrade, e.g. 7.6. Reasons:
    - If we leave the old source RHEL release set, dnf calls on the upgraded target RHEL would fail.
    - If we merely unset the release, users might end up updating the system to a newer version than the upgrade
      was supposed to be upgrading to.
    """

    name = 'enable_rhsm_target_repos'
    consumes = (UsedTargetRepositories,)
    produces = ()
    tags = (IPUWorkflowTag, FirstBootPhaseTag)

    def process(self):
        enablerhsmtargetrepos.set_rhsm_release()
        enablerhsmtargetrepos.enable_rhsm_repos()


# repos/system_upgrade/common/actors/filterrpmtransactionevents/actor.py

from leapp.actors import Actor
from leapp.models import (
    FilteredRpmTransactionTasks,
    InstalledRedHatSignedRPM,
    PESRpmTransactionTasks,
    RpmTransactionTasks
)
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class FilterRpmTransactionTasks(Actor):
    """
    Filter RPM transaction events based on installed RPM packages

    In order to calculate a working DNF Upgrade transaction, Leapp can collect data from multiple
    sources and find workarounds for possible problems. This actor will filter all collected
    workarounds and keep only those relevants to current system based on installed packages.
    """

    name = 'check_rpm_transaction_events'
    consumes = (PESRpmTransactionTasks, RpmTransactionTasks, InstalledRedHatSignedRPM,)
    produces = (FilteredRpmTransactionTasks,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        installed_pkgs = set()
        for rpm_pkgs in self.consume(InstalledRedHatSignedRPM):
            installed_pkgs.update([pkg.name for pkg in rpm_pkgs.items])

        local_rpms = set()
        to_install = set()
        to_remove = set()
        to_keep = set()
        to_upgrade = set()
        modules_to_enable = {}
        modules_to_reset = {}
        for event in self.consume(RpmTransactionTasks, PESRpmTransactionTasks):
            local_rpms.update(event.local_rpms)
            to_install.update(event.to_install)
            to_remove.update(installed_pkgs.intersection(event.to_remove))
            to_keep.update(installed_pkgs.intersection(event.to_keep))
            modules_to_enable.update({'{}:{}'.format(m.name, m.stream): m for m in event.modules_to_enable})
            modules_to_reset.update({'{}:{}'.format(m.name, m.stream): m for m in event.modules_to_reset})

        to_remove.difference_update(to_keep)

        # run upgrade for the rest of RH signed pkgs which we do not have rule for
        to_upgrade = installed_pkgs - (to_install | to_remove)

        self.produce(FilteredRpmTransactionTasks(
            local_rpms=list(local_rpms),
            to_install=list(to_install),
            to_remove=list(to_remove),
            to_keep=list(to_keep),
            to_upgrade=list(to_upgrade),
            modules_to_reset=list(modules_to_reset.values()),
            modules_to_enable=list(modules_to_enable.values())))


# repos/system_upgrade/common/actors/forcedefaultboottotargetkernelversion/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import forcedefaultboot
from leapp.models import InstalledTargetKernelVersion
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class ForceDefaultBootToTargetKernelVersion(Actor):
    """
    Ensure the default boot entry is set to the new target kernel

    This Actor ensure that the default entry in the boot loader is set to the newly installed kernel version.
    There have been cases when the default boot entry was not set to the default kernel version. In this case the
    actor will log a warning for debugging purposes.
    """

    name = 'force_default_boot_to_target_kernel_version'
    consumes = (InstalledTargetKernelVersion,)
    produces = ()
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        forcedefaultboot.process()


# repos/system_upgrade/common/actors/getenabledmodules/actor.py

from leapp.actors import Actor
from leapp.libraries.common.module import get_enabled_modules
from leapp.models import EnabledModules, Module
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class GetEnabledModules(Actor):
    """
    Provides data about which module streams are enabled on the source system.
    """

    name = 'get_enabled_modules'
    consumes = ()
    produces = (EnabledModules,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        modules = [Module(name=m.getName(), stream=m.getStream()) for m in get_enabled_modules()]
        self.produce(EnabledModules(modules=modules))


# repos/system_upgrade/common/actors/getinstalleddesktops/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.getinstalleddesktops import get_installed_desktops
from leapp.models import InstalledDesktopsFacts, InstalledRPM
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class GetInstalledDesktops(Actor):
    """
    Actor checks if kde or gnome desktop environments
    are installed and what desktop is default.
    """

    name = 'get_installed_desktops'
    consumes = (InstalledRPM,)
    produces = (InstalledDesktopsFacts,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        facts = get_installed_desktops()
        self.produce(InstalledDesktopsFacts(
            gnome_installed=facts["gnome_installed"],
            kde_installed=facts["kde_installed"]))


# repos/system_upgrade/common/actors/inhibitwhenluks/actor.py

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


class InhibitWhenLuks(Actor):
    """
    Check if any encrypted partitions is in use. If yes, inhibit the upgrade process.

    Upgrading system with encrypted partition is not supported.
    """

    name = 'check_luks_and_inhibit'
    consumes = (StorageInfo, CephInfo)
    produces = (Report,)
    tags = (ChecksPhaseTag, IPUWorkflowTag)

    def process(self):
        # If encrypted Ceph volumes present, check if there are more encrypted disk in lsblk than Ceph vol
        ceph_vol = []
        try:
            ceph_info = next(self.consume(CephInfo))
            if ceph_info:
                ceph_vol = ceph_info.encrypted_volumes[:]
                for storage_info in self.consume(StorageInfo):
                    for blk in storage_info.lsblk:
                        if blk.tp == 'crypt' and blk.name not in ceph_vol:
                            create_report([
                                reporting.Title('LUKS encrypted partition detected'),
                                reporting.Summary('Upgrading system with encrypted partitions is not supported'),
                                reporting.Severity(reporting.Severity.HIGH),
                                reporting.Groups([reporting.Groups.BOOT, reporting.Groups.ENCRYPTION]),
                                reporting.Groups([reporting.Groups.INHIBITOR]),
                            ])
                            break
        except StopIteration:
            for storage_info in self.consume(StorageInfo):
                for blk in storage_info.lsblk:
                    if blk.tp == 'crypt':
                        create_report([
                            reporting.Title('LUKS encrypted partition detected'),
                            reporting.Summary('Upgrading system with encrypted partitions is not supported'),
                            reporting.Severity(reporting.Severity.HIGH),
                            reporting.Groups([reporting.Groups.BOOT, reporting.Groups.ENCRYPTION]),
                            reporting.Groups([reporting.Groups.INHIBITOR]),
                        ])
                        break


# repos/system_upgrade/common/actors/ipascanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.ipascanner import is_ipa_client_configured, is_ipa_server_configured
from leapp.libraries.common.rpms import has_package
from leapp.models import InstalledRedHatSignedRPM, IpaInfo
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class IpaScanner(Actor):
    """
    Scan system for ipa-client and ipa-server status
    """

    name = "ipa_scanner"
    consumes = (InstalledRedHatSignedRPM,)
    produces = (IpaInfo,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        ipainfo = IpaInfo(
            has_client_package=has_package(
                InstalledRedHatSignedRPM, "ipa-client"
            ),
            is_client_configured=is_ipa_client_configured(),
            has_server_package=has_package(
                InstalledRedHatSignedRPM, "ipa-server"
            ),
            is_server_configured=is_ipa_server_configured(),
        )
        self.produce(ipainfo)


# repos/system_upgrade/common/actors/ipuworkflowconfig/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import ipuworkflowconfig
from leapp.models import IPUConfig
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag


class IPUWorkflowConfig(Actor):
    """
    IPU workflow config actor
    """

    name = 'ipu_workflow_config'
    consumes = ()
    produces = (IPUConfig, Report)
    tags = (IPUWorkflowTag,)

    def process(self):
        ipuworkflowconfig.produce_ipu_config(self)


# repos/system_upgrade/common/actors/kernelcmdlineconfig/actor.py

import os

from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor import kernelcmdlineconfig
from leapp.models import FirmwareFacts, InstalledTargetKernelVersion, KernelCmdlineArg, TargetKernelCmdlineArgTasks
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class KernelCmdlineConfig(Actor):
    """
    Append extra arguments to the target RHEL kernel command line
    """

    name = 'kernelcmdlineconfig'
    consumes = (KernelCmdlineArg, InstalledTargetKernelVersion, FirmwareFacts, TargetKernelCmdlineArgTasks)
    produces = ()
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):

        configs = None
        ff = next(self.consume(FirmwareFacts), None)
        if not ff:
            raise StopActorExecutionError(
                'Could not identify system firmware',
                details={'details': 'Actor did not receive FirmwareFacts message.'}
            )

        if ff.firmware == 'bios' and os.path.ismount('/boot/efi'):
            configs = ['/boot/grub2/grub.cfg', '/boot/efi/EFI/redhat/grub.cfg']
        kernelcmdlineconfig.modify_kernel_args_in_boot_cfg(configs)


# repos/system_upgrade/common/actors/loaddevicedriverdeprecationdata/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import deviceanddriverdeprecationdataload
from leapp.models import DeviceDriverDeprecationData
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class LoadDeviceDriverDeprecationData(Actor):
    """
    Loads deprecation data for drivers and devices (PCI & CPU)

    The data will either be loaded from the local /etc/leapp/files location or
    fetched from the Red Hat remote service providing this data.
    """

    name = 'load_device_driver_deprecation_data'
    consumes = ()
    produces = (DeviceDriverDeprecationData,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self, *args, **kwargs):
        deviceanddriverdeprecationdataload.process()


# repos/system_upgrade/common/actors/localreposinhibit/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.models import TargetOSInstallationImage, TMPTargetRepositoriesFacts, UsedTargetRepositories
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag
from leapp.utils.deprecation import suppress_deprecation


@suppress_deprecation(TMPTargetRepositoriesFacts)
class LocalReposInhibit(Actor):
    """Inhibits the upgrade if local repositories were found."""

    name = "local_repos_inhibit"
    consumes = (
        UsedTargetRepositories,
        TargetOSInstallationImage,
        TMPTargetRepositoriesFacts,
    )
    produces = (Report,)
    tags = (IPUWorkflowTag, TargetTransactionChecksPhaseTag)

    def collect_target_repoids_with_local_url(self, used_target_repos, target_repos_facts, target_iso):
        """Collects all repoids that have a local (file://) URL.

        UsedTargetRepositories doesn't contain baseurl attribute. So gathering
        them from model TMPTargetRepositoriesFacts.
        """
        used_target_repoids = set(repo.repoid for repo in used_target_repos.repos)
        iso_repoids = set(iso_repo.repoid for iso_repo in target_iso.repositories) if target_iso else set()

        target_repofile_data = (repofile.data for repofile in target_repos_facts.repositories)

        local_repoids = []
        for repo_data in target_repofile_data:
            for target_repo in repo_data:
                # Check only in repositories that are used and are not provided by the upgrade ISO, if any
                if target_repo.repoid not in used_target_repoids or target_repo.repoid in iso_repoids:
                    continue

                # Repo fields potentially containing local URLs have different importance, check based on their prio
                url_field_to_check = target_repo.mirrorlist or target_repo.metalink or target_repo.baseurl or ''

                if url_field_to_check.startswith("file://"):
                    local_repoids.append(target_repo.repoid)
        return local_repoids

    def process(self):
        used_target_repos = next(self.consume(UsedTargetRepositories), None)
        target_repos_facts = next(self.consume(TMPTargetRepositoriesFacts), None)
        target_iso = next(self.consume(TargetOSInstallationImage), None)

        if not used_target_repos or not target_repos_facts:
            return

        local_repoids = self.collect_target_repoids_with_local_url(used_target_repos, target_repos_facts, target_iso)
        if local_repoids:
            suffix, verb = ("y", "has") if len(local_repoids) == 1 else ("ies", "have")
            local_repoids_str = ", ".join(local_repoids)

            warn_msg = ("The following local repositor{suffix} {verb} been found: {local_repoids} "
                        "(their baseurl starts with file:///). Currently leapp does not support this option.")
            warn_msg = warn_msg.format(suffix=suffix, verb=verb, local_repoids=local_repoids_str)
            self.log.warning(warn_msg)

            reporting.create_report(
                [
                    reporting.Title("Local repositor{suffix} detected".format(suffix=suffix)),
                    reporting.Summary(warn_msg),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Groups([reporting.Groups.REPOSITORY]),
                    reporting.Groups([reporting.Groups.INHIBITOR]),
                    reporting.Remediation(
                        hint=(
                            "By using Apache HTTP Server you can expose "
                            "your local repository via http. See the linked "
                            "article for details. "
                        )
                    ),
                    reporting.ExternalLink(
                        title=(
                            "Customizing your Red Hat Enterprise Linux "
                            "in-place upgrade"
                        ),
                        url=(
                            "https://access.redhat.com/articles/4977891/"
                            "#repos-known-issues"
                        ),
                    ),
                ]
            )


# repos/system_upgrade/common/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
     * Subsystem sftp

    """

    name = 'read_openssh_config'
    consumes = ()
    produces = (OpenSshConfig, )
    tags = (FactsPhaseTag, IPUWorkflowTag, )

    def process(self):
        readopensshconfig.scan_sshd(self.produce)


# repos/system_upgrade/common/actors/opensshpermitrootlogincheck/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor.opensshpermitrootlogincheck import global_value, semantics_changes
from leapp.libraries.common.config.version import get_source_major_version
from leapp.libraries.stdlib import api
from leapp.models import OpenSshConfig, Report
from leapp.reporting import create_report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag

COMMON_REPORT_TAGS = [
    reporting.Groups.AUTHENTICATION,
    reporting.Groups.SECURITY,
    reporting.Groups.NETWORK,
    reporting.Groups.SERVICES
]

COMMON_RESOURCES = [
    reporting.RelatedResource('package', 'openssh-server'),
    reporting.RelatedResource('file', '/etc/ssh/sshd_config')
]


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 upgrade to the next major version of RHEL.

    The RHEL8 still provided default configuration that allowed root logins,
    which can lead to possible unwanted changes during the upgrade
    """
    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.'}
            )

        if get_source_major_version() == '7':
            self.process7to8(config)
        elif get_source_major_version() == '8':
            self.process8to9(config)
        else:
            api.current_logger().warning('Unknown source major version: {} (expecting 7 or 8)'
                                         .format(get_source_major_version()))

    def process7to8(self, config):
        # when the config was not modified, we can pass this check and let the
        # rpm handle the configuration file update
        if not config.modified:
            return

        # When the configuration does not contain *any* PermitRootLogin directive and
        # the configuration file was locally modified, it will not get updated by
        # RPM and the user might be locked away from the server with new default
        if not config.permit_root_login:
            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.Groups(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. '
                         'If this change is ok for you, add explicit '
                         '"PermitRootLogin prohibit-password" to your sshd_config '
                         'to ignore this inhibitor'
                ),
                reporting.Groups([reporting.Groups.INHIBITOR])
            ] + COMMON_RESOURCES)
            return

        # 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 passwords '
                    'so your server might get inaccessible.'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups(COMMON_REPORT_TAGS),
                reporting.Remediation(
                    hint='Consider using different user for administrative '
                         'logins or make sure your configuration file '
                         'contains the line "PermitRootLogin yes" '
                         'in global context if desired.'
                ),
                reporting.Groups([reporting.Groups.INHIBITOR])
            ] + COMMON_RESOURCES)

    def process8to9(self, config):
        # RHEL8 default sshd configuration file is not modified: It will get replaced by rpm and
        # root will no longer be able to connect through ssh. This will probably result in many
        # false positives so it will have to be waived a lot
        if not config.modified:
            create_report([
                reporting.Title('Possible problems with remote login using root account'),
                reporting.Summary(
                    'OpenSSH configuration file will get updated to RHEL9 '
                    'version, no longer allowing root login with password. '
                    'It is a good practice to use non-root administrative '
                    'user and non-password authentications, but if you rely '
                    'on the remote root login, this change can lock you out '
                    'of this system.'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups(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 a comment into the '
                         'sshd_config next to the "PermitRootLogin yes" directive '
                         'to prevent rpm replacing it during the upgrade.'
                ),
                reporting.Groups([reporting.Groups.INHIBITOR])
            ] + COMMON_RESOURCES)
        # If the configuration is modified and contains any directive allowing
        # root login (which is in default configuration), we are upgrading to
        # RHEL9 keeping the old "security policy", which might keep the root
        # login unexpectedly open. This might be just high priority warning
        if global_value(config, 'prohibit-password') == 'yes':
            create_report([
                reporting.Title('Remote root logins globally allowed using password'),
                reporting.Summary(
                    'RHEL9 no longer allows remote root logins, but the '
                    'server configuration explicitly overrides this default. '
                    'The configuration file will not be updated and root is '
                    'still going to be allowed to login with password. '
                    'This is not recommended and considered as a security risk.'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups(COMMON_REPORT_TAGS),
                reporting.Remediation(
                    hint='If you depend on remote root logins using passwords, '
                         'consider setting up a different user for remote '
                         'administration. Otherwise you can ignore this message.'
                )
            ] + COMMON_RESOURCES)


# repos/system_upgrade/common/actors/pcidevicesscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import pcidevicesscanner
from leapp.models import DetectedDeviceOrDriver, DeviceDriverDeprecationData, PCIDevices
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class PCIDevicesScanner(Actor):
    """
    Provides data about existing PCI Devices.

    After collecting data from lspci, a message with relevant data will be produced.
    """

    name = 'pci_devices_scanner'
    consumes = (DeviceDriverDeprecationData,)
    produces = (PCIDevices, DetectedDeviceOrDriver)
    tags = (IPUWorkflowTag, FactsPhaseTag,)

    def process(self):
        pcidevicesscanner.scan_pci_devices(self.produce)


# repos/system_upgrade/common/actors/persistentnetnames/actor.py

from leapp.actors import Actor
from leapp.libraries.common import persistentnetnames
from leapp.models import PersistentNetNamesFacts
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class PersistentNetNames(Actor):
    """
    Get network interface information for physical ethernet interfaces of the original system.

    Gather information like PCI topology, MAC address and interface name on the
    original system. Those data are provided through the PersistentNetNamesFacts
    model.

    See the persistentnetnamesinitramfs actor that is very same but processed
    during early phases in initrams to gather the same data but using already
    new kernel of the target system to be able to reflect changes affected
    by the new kernel (see the PersistentNetNamesConfig actor).
    """

    name = 'persistentnetnames'
    consumes = ()
    produces = (PersistentNetNamesFacts, )
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        self.produce(PersistentNetNamesFacts(interfaces=list(persistentnetnames.interfaces())))


# repos/system_upgrade/common/actors/persistentnetnamesconfig/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import persistentnetnamesconfig
from leapp.models import (
    InitrdIncludes,
    PersistentNetNamesFacts,
    PersistentNetNamesFactsInitramfs,
    RenamedInterfaces,
    TargetInitramfsTasks
)
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag
from leapp.utils.deprecation import suppress_deprecation


@suppress_deprecation(InitrdIncludes)
class PersistentNetNamesConfig(Actor):
    """
    Generate udev persistent network naming configuration

    This actor generates systemd-udevd link files for each physical ethernet interface present on RHEL-7
    in case we notice that interface name differs on RHEL-8. Link file configuration will assign RHEL-7 version of
    a name. Actors produces list of interfaces which changed name between RHEL-7 and RHEL-8.
    """

    name = 'persistentnetnamesconfig'
    consumes = (PersistentNetNamesFacts, PersistentNetNamesFactsInitramfs)
    produces = (RenamedInterfaces, InitrdIncludes, TargetInitramfsTasks)
    tags = (ApplicationsPhaseTag, IPUWorkflowTag)
    initrd_files = []

    def process(self):
        persistentnetnamesconfig.process()


# repos/system_upgrade/common/actors/persistentnetnamesdisable/actor.py

import re

from leapp import reporting
from leapp.actors import Actor
from leapp.models import KernelCmdlineArg, PersistentNetNamesFacts
from leapp.reporting import create_report, Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class PersistentNetNamesDisable(Actor):
    """
    Disable systemd-udevd persistent network naming on machine with single eth0 NIC
    """

    name = 'persistentnetnamesdisable'
    consumes = (PersistentNetNamesFacts,)
    produces = (KernelCmdlineArg, Report)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def ethX_count(self, interfaces):
        ethX = re.compile('eth[0-9]+')
        count = 0

        for i in interfaces:
            if ethX.match(i.name):
                count = count + 1
        return count

    def single_eth0(self, interfaces):
        return len(interfaces) == 1 and interfaces[0].name == 'eth0'

    def disable_persistent_naming(self):
        self.log.info("Single eth0 network interface detected. Appending 'net.ifnames=0' to RHEL-8 kernel commandline")
        self.produce(KernelCmdlineArg(**{'key': 'net.ifnames', 'value': '0'}))

    def process(self):
        interfaces = next(self.consume(PersistentNetNamesFacts)).interfaces

        if self.single_eth0(interfaces):
            self.disable_persistent_naming()
        elif len(interfaces) > 1 and self.ethX_count(interfaces) > 0:
            create_report([
                reporting.Title('Unsupported network configuration'),
                reporting.Summary(
                    'Detected multiple physical network interfaces where one or more use kernel naming (e.g. eth0). '
                    'Upgrade process can not continue because stability of names can not be guaranteed. '
                    'Please read the article at https://access.redhat.com/solutions/4067471 for more information.'
                ),
                reporting.ExternalLink(
                    title='How to perform an in-place upgrade to RHEL 8 when using kernel NIC names on RHEL 7',
                    url='https://access.redhat.com/solutions/4067471'
                ),
                reporting.Remediation(
                    hint='Rename all ethX network interfaces following the attached KB solution article.'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups([reporting.Groups.NETWORK]),
                reporting.Groups([reporting.Groups.INHIBITOR])
            ])


# repos/system_upgrade/common/actors/persistentnetnamesinitramfs/actor.py

from leapp.actors import Actor
from leapp.libraries.common import persistentnetnames
from leapp.models import PersistentNetNamesFactsInitramfs
from leapp.tags import IPUWorkflowTag, LateTestsPhaseTag


class PersistentNetNamesInitramfs(Actor):
    """
    Get network interface information for physical ethernet interfaces with the new kernel in initramfs.

    This actor does exactly the same job as PersistentNetNames actor except that it runs in a later phase.
    """

    name = 'persistentnetnamesinitramfs'
    consumes = ()
    produces = (PersistentNetNamesFactsInitramfs, )
    tags = (LateTestsPhaseTag, IPUWorkflowTag)

    def process(self):
        self.produce(PersistentNetNamesFactsInitramfs(interfaces=list(persistentnetnames.interfaces())))


# repos/system_upgrade/common/actors/peseventsscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.pes_events_scanner import process
from leapp.models import (
    EnabledModules,
    InstalledRedHatSignedRPM,
    PESRpmTransactionTasks,
    RepositoriesBlacklisted,
    RepositoriesFacts,
    RepositoriesMapping,
    RepositoriesSetupTasks,
    RHUIInfo,
    RpmTransactionTasks
)
from leapp.reporting import Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class PesEventsScanner(Actor):
    """
    Provides data about package events from Package Evolution Service.

    After collecting data from a provided JSON file containing Package Evolution Service events, a
    message with relevant data will be produced to help DNF Upgrade transaction calculation.
    """

    name = 'pes_events_scanner'
    consumes = (
        EnabledModules,
        InstalledRedHatSignedRPM,
        RepositoriesBlacklisted,
        RepositoriesFacts,
        RepositoriesMapping,
        RHUIInfo,
        RpmTransactionTasks,
    )
    produces = (PESRpmTransactionTasks, RepositoriesSetupTasks, Report)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        process()


# repos/system_upgrade/common/actors/preparepythonworkround/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.workaround import apply_python3_workaround
from leapp.tags import IPUWorkflowTag, RPMUpgradePhaseTag


class PreparePythonWorkround(Actor):
    """
    Prepare environment to be able to run leapp with Python3 in initrd.

    These are the current necessary steps to be able to run Leapp with Python3.
    Basically, we create directory (now /root/tmp_leapp_py3/). We will put
    symlinks inside which will point to leapp python packages. Additionally,
    we create new script that will import expected modules and run leapp again.
    """

    name = 'prepare_python_workround'
    consumes = ()
    produces = ()
    tags = (IPUWorkflowTag, RPMUpgradePhaseTag)

    def process(self):
        apply_python3_workaround()


# repos/system_upgrade/common/actors/redhatsignedrpmcheck/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.redhatsignedrpmcheck import check_unsigned_packages
from leapp.models import InstalledUnsignedRPM
from leapp.reporting import Report
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag


class RedHatSignedRpmCheck(Actor):
    """
    Check if there are packages not signed by Red Hat in use. If yes, warn user about it.

    If any any installed RPM package does not contain a valid signature from Red Hat, a message
    containing a warning is produced.
    """

    name = 'red_hat_signed_rpm_check'
    consumes = (InstalledUnsignedRPM,)
    produces = (Report,)
    tags = (IPUWorkflowTag, ChecksPhaseTag)

    def process(self):
        check_unsigned_packages()


# repos/system_upgrade/common/actors/redhatsignedrpmscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.common import rhui
from leapp.models import InstalledRedHatSignedRPM, InstalledRPM, InstalledUnsignedRPM
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class RedHatSignedRpmScanner(Actor):
    """Provide data about installed RPM Packages signed by Red Hat.

    After filtering the list of installed RPM packages by signature, a message
    with relevant data will be produced.
    """

    name = 'red_hat_signed_rpm_scanner'
    consumes = (InstalledRPM,)
    produces = (InstalledRedHatSignedRPM, InstalledUnsignedRPM,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        RH_SIGS = ['199e2f91fd431d51',
                   '5326810137017186',
                   '938a80caf21541eb',
                   'fd372689897da07a',
                   '45689c882fa658e0']

        signed_pkgs = InstalledRedHatSignedRPM()
        unsigned_pkgs = InstalledUnsignedRPM()

        env_vars = self.configuration.leapp_env_vars
        # if we start upgrade with LEAPP_DEVEL_RPMS_ALL_SIGNED=1, we consider
        # all packages to be signed
        all_signed = [
            env
            for env in env_vars
            if env.name == 'LEAPP_DEVEL_RPMS_ALL_SIGNED' and env.value == '1'
        ]

        def has_rhsig(pkg):
            return any(key in pkg.pgpsig for key in RH_SIGS)

        def is_gpg_pubkey(pkg):
            """Check if gpg-pubkey pkg exists or LEAPP_DEVEL_RPMS_ALL_SIGNED=1

            gpg-pubkey is not signed as it would require another package
            to verify its signature
            """
            return (    # pylint: disable-msg=consider-using-ternary
                    pkg.name == 'gpg-pubkey'
                    and pkg.packager.startswith('Red Hat, Inc.')
                    or all_signed
            )

        def has_katello_prefix(pkg):
            """Whitelist the katello package."""
            return pkg.name.startswith('katello-ca-consumer')

        upg_path = rhui.get_upg_path()
        # AWS RHUI packages do not have to be whitelisted because they are signed by RedHat
        whitelisted_cloud_flavours = ('azure', 'azure-eus', 'azure-sap', 'azure-sap-apps', 'google', 'google-sap')
        whitelisted_cloud_pkgs = {
            rhui.RHUI_CLOUD_MAP[upg_path].get(flavour, {}).get('src_pkg') for flavour in whitelisted_cloud_flavours
        }
        whitelisted_cloud_pkgs.update(
            rhui.RHUI_CLOUD_MAP[upg_path].get(flavour, {}).get('target_pkg') for flavour in whitelisted_cloud_flavours
        )

        for rpm_pkgs in self.consume(InstalledRPM):
            for pkg in rpm_pkgs.items:
                if any(
                    [
                        has_rhsig(pkg),
                        is_gpg_pubkey(pkg),
                        has_katello_prefix(pkg),
                        pkg.name in whitelisted_cloud_pkgs,
                    ]
                ):
                    signed_pkgs.items.append(pkg)
                    continue

                unsigned_pkgs.items.append(pkg)

        self.produce(signed_pkgs)
        self.produce(unsigned_pkgs)


# repos/system_upgrade/common/actors/removebootfiles/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.removebootfiles import remove_boot_files
from leapp.models import BootContent
from leapp.tags import IPUWorkflowTag, PreparationPhaseTag


class RemoveBootFiles(Actor):
    """
    Remove Leapp provided initramfs from boot partition.

    Since Leapp provided initramfs and kernel are already loaded into RAM at this phase, remove
    them to have as little space requirements for boot partition as possible.
    """

    name = 'remove_boot_files'
    consumes = (BootContent,)
    produces = ()
    tags = (IPUWorkflowTag, PreparationPhaseTag)

    def process(self):
        remove_boot_files()


# repos/system_upgrade/common/actors/removeresumeservice/actor.py

import errno
import os

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.stdlib import api, run
from leapp.reporting import create_report, Report
from leapp.tags import FirstBootPhaseTag, IPUWorkflowTag


class RemoveSystemdResumeService(Actor):
    """
    Remove systemd service to launch Leapp.

    After system was rebooted and process resumed, this service is not necessary anymore.
    """

    name = 'remove_systemd_resume_service'
    consumes = ()
    produces = (Report,)
    tags = (FirstBootPhaseTag.After, IPUWorkflowTag)

    def process(self):
        service_name = 'leapp_resume.service'
        if os.path.isfile('/etc/systemd/system/{}'.format(service_name)):
            run(['systemctl', 'disable', service_name])
            paths_to_unlink = [
                '/etc/systemd/system/{}'.format(service_name),
                '/etc/systemd/system/default.target.wants/{}'.format(service_name),
            ]
            for path in paths_to_unlink:
                try:
                    os.unlink(path)
                except OSError as e:
                    api.current_logger().debug('Failed removing {}: {}'.format(path, str(e)))
                    if e.errno != errno.ENOENT:
                        raise

        create_report([
            reporting.Title('"{}" service deleted'.format(service_name)),
            reporting.Summary(
                '"{}" was taking care of resuming upgrade process '
                'after the first reboot.'.format(service_name)),
            reporting.Groups([reporting.Groups.UPGRADE_PROCESS]),
        ])


# repos/system_upgrade/common/actors/removeupgradebootentry/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.removeupgradebootentry import remove_boot_entry
from leapp.models import BootContent, FirmwareFacts
from leapp.tags import InitRamStartPhaseTag, IPUWorkflowTag


class RemoveUpgradeBootEntry(Actor):
    """
    Remove boot entry for Leapp provided initramfs.

    After upgrade process used initramfs after reboot, this entry is not necessary anymore.
    """

    name = 'remove_upgrade_boot_entry'
    consumes = (BootContent, FirmwareFacts)
    produces = ()
    tags = (IPUWorkflowTag, InitRamStartPhaseTag)

    def process(self):
        remove_boot_entry()


# repos/system_upgrade/common/actors/reportsettargetrelease/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import reportsettargetrelease
from leapp.models import Report
from leapp.tags import IPUWorkflowTag, TargetTransactionChecksPhaseTag


class ReportSetTargetRelease(Actor):
    """
    Reports information related to the release set in the subscription-manager after the upgrade.

    When using Red Hat subscription-manager (RHSM), the release is set by default
    to the target version release. In case of skipping the RHSM (--no-rhsm), the
    release stays as it was on the source RHEL and user has to handle it manually
    after the upgrade.
    """

    name = 'report_set_target_release'
    consumes = ()
    produces = (Report,)
    tags = (IPUWorkflowTag, TargetTransactionChecksPhaseTag)

    def process(self):
        reportsettargetrelease.process()


# repos/system_upgrade/common/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/common/actors/repositoriesmapping/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.repositoriesmapping import scan_repositories
from leapp.models import RepositoriesMapping
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class RepositoriesMappingScanner(Actor):
    """
    Produces message containing repository mapping based on provided file.

    The actor filters out data irrelevant to the current IPU (data with different
    source/target major versions) from the raw repository mapping data.
    """

    name = 'repository_mapping'
    consumes = ()
    produces = (RepositoriesMapping,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scan_repositories()


# repos/system_upgrade/common/actors/rootscanner/actor.py

import os

import six

from leapp.actors import Actor
from leapp.models import InvalidRootSubdirectory, RootDirectory, RootSubdirectory
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class RootScanner(Actor):
    """
    Scan the system root directory and produce a message containing
    information about its subdirectories.
    """

    name = 'root_scanner'
    consumes = ()
    produces = (RootDirectory,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        subdirs = []
        invalid_subdirs = []

        def _create_a_subdir(subdir_cls, name, path):
            if os.path.islink(path):
                return subdir_cls(name=name, target=os.readlink(path))
            return subdir_cls(name=name)

        for subdir in os.listdir('/'):
            # Note(ivasilev) non-utf encoded string will appear as byte strings
            if isinstance(subdir, six.binary_type):
                invalid_subdirs.append(_create_a_subdir(InvalidRootSubdirectory, subdir, os.path.join(b'/', subdir)))
            else:
                subdirs.append(_create_a_subdir(RootSubdirectory, subdir, os.path.join('/', subdir)))

        self.produce(RootDirectory(items=subdirs, invalid_items=invalid_subdirs))


# repos/system_upgrade/common/actors/rpmscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import rpmscanner
from leapp.models import InstalledRPM
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class RpmScanner(Actor):
    """
    Provides data about installed RPM Packages.

    After collecting data from RPM query, a message with relevant data will be produced.
    """

    name = 'rpm_scanner'
    consumes = ()
    produces = (InstalledRPM,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        rpmscanner.process()


# repos/system_upgrade/common/actors/rpmtransactionconfigtaskscollector/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.rpmtransactionconfigtaskscollector import load_tasks
from leapp.models import InstalledRedHatSignedRPM, RpmTransactionTasks
from leapp.tags import FactsPhaseTag, IPUWorkflowTag

CONFIGURATION_BASE_PATH = '/etc/leapp/transaction'


class RpmTransactionConfigTasksCollector(Actor):
    """
    Provides additional RPM transaction tasks from /etc/leapp/transaction.

    After collecting task data from /etc/leapp/transaction directory, a message with relevant data
    will be produced.
    """

    name = 'rpm_transaction_config_tasks_collector'
    consumes = (InstalledRedHatSignedRPM,)
    produces = (RpmTransactionTasks,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        self.produce(load_tasks(CONFIGURATION_BASE_PATH, self.log))


# repos/system_upgrade/common/actors/scanclienablerepo/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scanclienablerepo
from leapp.models import CustomTargetRepository
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanCLIenablrepo(Actor):
    """
    Produce CustomTargetRepository based on the LEAPP_ENABLE_REPOS in config.
    """

    name = 'scanclienablerepo'
    consumes = ()
    produces = (CustomTargetRepository)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        scanclienablerepo.process()


# repos/system_upgrade/common/actors/scancpu/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scancpu
from leapp.models import CPUInfo, DetectedDeviceOrDriver, DeviceDriverDeprecationData
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanCPU(Actor):
    """Scan CPUs of the machine."""

    name = 'scancpu'
    consumes = (DeviceDriverDeprecationData,)
    produces = (CPUInfo, DetectedDeviceOrDriver)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self, *args, **kwargs):
        scancpu.process()


# repos/system_upgrade/common/actors/scancustomrepofile/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scancustomrepofile
from leapp.models import CustomTargetRepository, CustomTargetRepositoryFile
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanCustomRepofile(Actor):
    """
    Scan the custom /etc/leapp/files/leapp_upgrade_repositories.repo repo file.

    This is the official path where to put the YUM/DNF repository file with
    custom repositories for the target system. These repositories will be used
    automatically for the in-place upgrade despite the enable/disable settings.

    Additionally the CustomTargetRepositoryFile message is produced if the file
    exists to let the other actors know they should handle the file as well.

    If the file doesn't exist, nothing happens.
    """

    name = 'scan_custom_repofile'
    consumes = ()
    produces = (CustomTargetRepository, CustomTargetRepositoryFile)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        scancustomrepofile.process()


# repos/system_upgrade/common/actors/scandasd/actor.py


from leapp.actors import Actor
from leapp.libraries.actor import scandasd
from leapp.models import TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanDASD(Actor):
    """
    In case of s390x architecture, check whether DASD is used.

    The current check is based just on existence of the /etc/dasd.conf file.
    If it exists, produce UpgradeInitramfsTasks msg to ensure the file
    is available inside the target userspace to be able to generate the
    upgrade init ramdisk correctly.
    """

    name = 'scandasd'
    consumes = ()
    produces = (TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scandasd.process()


# repos/system_upgrade/common/actors/scanfilesfortargetuserspace/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.scanfilesfortargetuserspace import scan_files_to_copy
from leapp.models import TargetUserSpacePreupgradeTasks
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanFilesForTargetUserspace(Actor):
    """
    Scan the source system and identify files that will be copied into the target userspace when it is created.
    """

    name = 'scan_files_for_target_userspace'
    consumes = ()
    produces = (TargetUserSpacePreupgradeTasks,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        scan_files_to_copy()


# repos/system_upgrade/common/actors/scaninstalledtargetkernelversion/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scankernel
from leapp.models import InstalledTargetKernelVersion, TransactionCompleted
from leapp.tags import IPUWorkflowTag, RPMUpgradePhaseTag


class ScanInstalledTargetKernelVersion(Actor):
    """
    Scan for the version of the newly installed kernel

    This actor will query rpm for all kernel packages and reports the first
    matching target system kernel RPM. In case the RHEL Real Time has been detected on
    the original system, the kernel-rt rpm is searched. If the rpm is missing,
    fallback for standard kernel RPM.
    """

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

    def process(self):
        scankernel.process()


# repos/system_upgrade/common/actors/scankernelcmdline/actor.py

from leapp.actors import Actor
from leapp.libraries.stdlib import run
from leapp.models import KernelCmdline, KernelCmdlineArg
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanKernelCmdline(Actor):
    """
    No documentation has been provided for the scan_kernel_cmdline actor.
    """

    name = 'scan_kernel_cmdline'
    consumes = ()
    produces = (KernelCmdline,)
    tags = (FactsPhaseTag, IPUWorkflowTag)

    def process(self):
        cmdline = run(['cat', '/proc/cmdline'])['stdout'].strip()
        parameters = []
        for parameter in cmdline.split(' '):
            if '=' in parameter:
                kv = parameter.split('=')
                parameters.append(KernelCmdlineArg(key=kv[0], value=kv[1]))
            else:
                parameters.append(KernelCmdlineArg(key=parameter))
        self.produce(KernelCmdline(parameters=parameters))


# repos/system_upgrade/common/actors/scanmemory/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scanmemory
from leapp.models import MemoryInfo
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanMemory(Actor):
    """Scan Memory of the machine."""

    name = 'scanmemory'
    consumes = ()
    produces = (MemoryInfo,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scanmemory.process()


# repos/system_upgrade/common/actors/scanpkgmanager/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scanpkgmanager
from leapp.models import PkgManagerInfo
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanPkgManager(Actor):
    """
    Provides data about package manager (yum/dnf)
    """

    name = 'scan_pkg_manager'
    consumes = ()
    produces = (PkgManagerInfo,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scanpkgmanager.process()


# repos/system_upgrade/common/actors/scansaphana/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.scansaphana import perform_sap_hana_scan
from leapp.models import SapHanaInfo
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanSapHana(Actor):
    """
    Gathers information related to SAP HANA instances on the system.

    This actor collects information from SAP HANA installations and produces a message containing the details.
    The actor will determine whether SAP HANA is installed, running and which version is present on the system.
    """

    name = 'scan_sap_hana'
    consumes = ()
    produces = (SapHanaInfo,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        perform_sap_hana_scan()


# repos/system_upgrade/common/actors/scansubscriptionmanagerinfo/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scanrhsm
from leapp.models import RHSMInfo
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanSubscriptionManagerInfo(Actor):
    """
    Scans the current system for subscription manager information

    Retrieves information about enabled and available repositories, attached SKUs, product certificates and release
    from the current system without modifying it.
    """

    name = 'scan_subscription_manager_info'
    consumes = ()
    produces = (RHSMInfo,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scanrhsm.scan()


# repos/system_upgrade/common/actors/scantargetiso/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import scan_target_os_iso
from leapp.models import CustomTargetRepository, TargetOSInstallationImage
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanTargetISO(Actor):
    """Scans the provided target OS ISO image to use as a content source for the IPU, if any."""

    name = 'scan_target_os_image'
    consumes = ()
    produces = (CustomTargetRepository, TargetOSInstallationImage,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scan_target_os_iso.inform_ipu_about_request_to_use_target_iso()


# repos/system_upgrade/common/actors/scanzfcp/actor.py


from leapp.actors import Actor
from leapp.libraries.actor import scanzfcp
from leapp.models import TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class ScanZFCP(Actor):
    """
    In case of s390x architecture, check whether ZFCP is used.

    The current check is based just on existence of the /etc/zfcp.conf file.
    If it exists, produce UpgradeInitramfsTasks msg to ensure the file
    is available inside the target userspace to be able to generate the
    upgrade init ramdisk correctly.
    """

    name = 'scanzfcp'
    consumes = ()
    produces = (TargetUserSpaceUpgradeTasks, UpgradeInitramfsTasks)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        scanzfcp.process()


# repos/system_upgrade/common/actors/scheduleselinuxrelabeling/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.models import SelinuxRelabelDecision
from leapp.reporting import create_report, Report
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag

COMMON_REPORT_TAGS = [reporting.Groups.SELINUX]

related = [reporting.RelatedResource('file', '/.autorelabel')]


class ScheduleSeLinuxRelabeling(Actor):
    """
    Schedule SELinux relabelling.

    If SELinux status was set to permissive or enforcing, a relabelling is necessary.
    """

    name = 'schedule_se_linux_relabelling'
    consumes = (SelinuxRelabelDecision,)
    produces = (Report,)
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        for decision in self.consume(SelinuxRelabelDecision):
            if decision.set_relabel:
                try:
                    with open('/.autorelabel', 'w'):
                        pass
                    create_report([
                        reporting.Title('SElinux scheduled for relabelling'),
                        reporting.Summary(
                            '/.autorelabel file touched on root in order to schedule SElinux relabelling.'),
                        reporting.Severity(reporting.Severity.INFO),
                        reporting.Groups(COMMON_REPORT_TAGS),
                    ] + related)

                except EnvironmentError as e:
                    # FIXME: add an "action required" flag later
                    create_report([
                        reporting.Title('Could not schedule SElinux for relabelling'),
                        reporting.Summary('/.autorelabel file could not be created: {}.'.format(e)),
                        reporting.Severity(reporting.Severity.HIGH),
                        reporting.Groups(COMMON_REPORT_TAGS),
                        reporting.Remediation(
                            hint='Please set autorelabelling manually after the upgrade.'
                        ),
                        reporting.Groups([reporting.Groups.FAILURE])
                    ] + related)
                    self.log.critical('Could not schedule SElinux for relabelling: %s.' % e)


# repos/system_upgrade/common/actors/setetcreleasever/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import setetcreleasever
from leapp.models import PkgManagerInfo, RHUIInfo
from leapp.tags import ApplicationsPhaseTag, IPUWorkflowTag


class SetEtcReleaseVer(Actor):
    """
    Release version in /etc/dnf/vars/releasever will be set to the current target release

    If Leapp detects "releasever" variable is either configured through DNF/YUM configuration
    file and/or the system is using RHUI infrastructure, release version will be set to the target
    release version in order to avoid issues with repofile URLs (when --release option is not provided)
    in cases where there is the previous major.minor version value in the configuration. This will also
    ensure the system stays on the target version after the upgrade.
    """

    name = 'set_etc_releasever'
    consumes = (PkgManagerInfo, RHUIInfo)
    produces = ()
    tags = (IPUWorkflowTag, ApplicationsPhaseTag)

    def process(self):
        setetcreleasever.process()


# repos/system_upgrade/common/actors/setpermissiveselinux/actor.py

from leapp import reporting
from leapp.actors import Actor
from leapp.libraries.actor.setpermissiveselinux import selinux_set_permissive
from leapp.models import SelinuxPermissiveDecision
from leapp.reporting import create_report, Report
from leapp.tags import FinalizationPhaseTag, IPUWorkflowTag


class SetPermissiveSelinux(Actor):
    """
    Set SELinux mode.

    In order to proceed with Upgrade process, SELinux should be set into permissive mode if it was
    in enforcing mode.
    """

    name = 'set_permissive_se_linux'
    consumes = (SelinuxPermissiveDecision,)
    produces = (Report,)
    tags = (FinalizationPhaseTag, IPUWorkflowTag)

    def process(self):
        for decision in self.consume(SelinuxPermissiveDecision):
            if decision.set_permissive:
                success, err_msg = selinux_set_permissive()
                if not success:
                    # FIXME: add an "action required" flag later
                    create_report([
                        reporting.Title('Could not set SElinux into permissive mode'),
                        reporting.Summary('{}'.format(err_msg)),
                        reporting.Severity(reporting.Severity.HIGH),
                        reporting.Groups([reporting.Groups.SELINUX, reporting.Groups.SECURITY]),
                        reporting.Groups([reporting.Groups.FAILURE]),
                        reporting.RelatedResource('file', '/etc/selinux/config')
                    ])
                    self.log.critical('Could not set SElinux into permissive mode: %s.' % err_msg)


# repos/system_upgrade/common/actors/setuptargetrepos/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import setuptargetrepos
from leapp.models import (
    CustomTargetRepository,
    RepositoriesBlacklisted,
    RepositoriesFacts,
    RepositoriesMapping,
    RepositoriesSetupTasks,
    RHUIInfo,
    SkippedRepositories,
    TargetRepositories,
    UsedRepositories
)
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class SetupTargetRepos(Actor):
    """
    Produces list of repositories that should be available to be used by Upgrade process.

    Based on current set of Red Hat Enterprise Linux repositories, produces the list of target
    repositories. Additionally process request to use custom repositories during the upgrade
    transaction.
    """

    name = 'setuptargetrepos'
    consumes = (CustomTargetRepository,
                RepositoriesSetupTasks,
                RepositoriesMapping,
                RepositoriesFacts,
                RepositoriesBlacklisted,
                RHUIInfo,
                UsedRepositories)
    produces = (TargetRepositories, SkippedRepositories)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        setuptargetrepos.process()


# repos/system_upgrade/common/actors/storagescanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import storagescanner
from leapp.models import StorageInfo
from leapp.reporting import Report
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class StorageScanner(Actor):
    """
    Provides data about storage settings.

    After collecting data from tools like mount, lsblk, pvs, vgs and lvdisplay, and relevant files
    under /proc/partitions and /etc/fstab, a message with relevant data will be produced.
    """

    name = 'storage_scanner'
    consumes = ()
    produces = (Report, StorageInfo)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        self.produce(storagescanner.get_storage_info())


# repos/system_upgrade/common/actors/systemfacts/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import systemfacts
from leapp.libraries.common.config import architecture
from leapp.models import (
    ActiveKernelModulesFacts,
    DefaultGrubInfo,
    FirewallsFacts,
    FirmwareFacts,
    GroupsFacts,
    GrubCfgBios,
    Report,
    RepositoriesFacts,
    SELinuxFacts,
    SysctlVariablesFacts,
    UsersFacts
)
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class SystemFactsActor(Actor):
    """
    Provides data about many facts from system.

    After collecting data from multiple tools, messages with relevant data will be produced to
    describe:
      - Sysctl variables;
      - Active Linux Kernel Modules;
      - Users;
      - Groups;
      - Package repositories;
      - SELinux status;
      - Firewalls status.
    """

    name = 'system_facts'
    consumes = ()
    produces = (
        SysctlVariablesFacts,
        ActiveKernelModulesFacts,
        UsersFacts,
        GroupsFacts,
        RepositoriesFacts,
        SELinuxFacts,
        FirewallsFacts,
        FirmwareFacts,
        DefaultGrubInfo,
        GrubCfgBios,
        Report
    )
    tags = (IPUWorkflowTag, FactsPhaseTag,)

    def process(self):
        self.produce(systemfacts.get_sysctls_status())
        self.produce(systemfacts.get_active_kernel_modules_status(self.log))
        self.produce(systemfacts.get_system_users_status())
        self.produce(systemfacts.get_system_groups_status())
        self.produce(systemfacts.get_repositories_status())
        self.produce(systemfacts.get_selinux_status())
        self.produce(systemfacts.get_firewalls_status())
        self.produce(systemfacts.get_firmware())

        if not architecture.matches_architecture(architecture.ARCH_S390X):
            self.produce(systemfacts.get_default_grub_conf())

        bios_grubcfg_details = systemfacts.get_bios_grubcfg_details()
        if bios_grubcfg_details:
            self.produce(bios_grubcfg_details)


# repos/system_upgrade/common/actors/targetuserspacecreator/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import userspacegen
from leapp.libraries.common.config import get_env, version
from leapp.models import RequiredTargetUserspacePackages  # deprecated
from leapp.models import TMPTargetRepositoriesFacts  # deprecated
from leapp.models import (
    CustomTargetRepositoryFile,
    PkgManagerInfo,
    Report,
    RepositoriesFacts,
    RepositoriesMapping,
    RHSMInfo,
    RHUIInfo,
    StorageInfo,
    TargetOSInstallationImage,
    TargetRepositories,
    TargetUserSpaceInfo,
    TargetUserSpacePreupgradeTasks,
    UsedTargetRepositories,
    XFSPresence
)
from leapp.tags import IPUWorkflowTag, TargetTransactionFactsPhaseTag


# @suppress_deprecation(RequiredTargetUserspacePackages, TMPTargetRepositoriesFacts)
class TargetUserspaceCreator(Actor):
    """
    Initializes a directory to be populated as a minimal environment to run binaries from the target system.

    The target userspace is set up in a directory so one can run it in a containerized environment to perform tasks
    as if the system running would be the target system. This allows us to use the target system RPM stack including
    DNF which gives us the ability to use RPM features from the target system.
    The userspace environment is also used to generate a initram disk with dracut using the target system binaries
    such as kernel, systemd etc etc
    """

    name = 'target_userspace_creator'
    consumes = (
        CustomTargetRepositoryFile,
        RHSMInfo,
        RHUIInfo,
        RepositoriesFacts,
        RepositoriesMapping,
        RequiredTargetUserspacePackages,
        StorageInfo,
        TargetOSInstallationImage,
        TargetRepositories,
        TargetUserSpacePreupgradeTasks,
        XFSPresence,
        PkgManagerInfo,
    )
    produces = (TargetUserSpaceInfo, UsedTargetRepositories, Report, TMPTargetRepositoriesFacts,)
    tags = (IPUWorkflowTag, TargetTransactionFactsPhaseTag)

    def process(self):
        skip_check = get_env('LEAPP_DEVEL_SKIP_CHECK_OS_RELEASE', False)
        if (skip_check or version.is_supported_version()) and next(self.consume(RepositoriesMapping), None):
            userspacegen.perform()


# repos/system_upgrade/common/actors/transactionworkarounds/actor.py

from leapp.actors import Actor
from leapp.libraries.actor import transactionworkarounds
from leapp.models import RpmTransactionTasks
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class TransactionWorkarounds(Actor):
    """
    Provides additional RPM transaction tasks based on bundled RPM packages.

    After collecting bundled RPM packages, a message with relevant data will be produced.
    """

    name = 'transaction_workarounds'
    consumes = ()
    produces = (RpmTransactionTasks,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        transactionworkarounds.process()


# repos/system_upgrade/common/actors/unsupportedupgradecheck/actor.py

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


class UnsupportedUpgradeCheck(Actor):
    """
    Checks environment variables and produces a warning report if the upgrade is unsupported.

    Upgrade is unsupported if any LEAPP_DEVEL_* variable is used or an experimental actor is enabled.
    This can be overridden by setting the variable LEAPP_UNSUPPORTED (at user's own risk).
    """

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

    def process(self):
        leapp_vars = self.configuration.leapp_env_vars
        devel_vars = [v for v in leapp_vars if v.name.startswith('LEAPP_DEVEL_')]
        experimental = bool([v for v in leapp_vars if v.name == 'LEAPP_EXPERIMENTAL' and v.value == '1'])
        override = bool([v for v in leapp_vars if v.name == 'LEAPP_UNSUPPORTED' and v.value == '1'])

        if override:
            reporting.create_report([
                reporting.Title('Upgrade is unsupported'),
                reporting.Summary(
                    'Environment variable LEAPP_UNSUPPORTED has been detected. A successful and safe '
                    'upgrade process cannot be guaranteed. From now on you are continuing at your own '
                    'risk.\n'
                ),
                reporting.Severity(reporting.Severity.HIGH),
                reporting.Groups([reporting.Groups.UPGRADE_PROCESS, reporting.Groups.SANITY]),
            ])

        else:
            if devel_vars:
                reporting.create_report([
                    reporting.Title('Upgrade inhibited due to usage of development variables'),
                    reporting.Summary(
                        'One or more environment variables in the form of LEAPP_DEVEL_* have been detected.\n'
                        'These variables are for development purposes only and can interfere with the upgrade '
                        'process in unexpected ways. As such, a successful and safe upgrade process cannot be '
                        'guaranteed and the upgrade is unsupported.\n'
                        'You can bypass this error by setting the LEAPP_UNSUPPORTED variable but by doing so, '
                        'you continue at your own risk.\n'
                        'Found development variables:\n- {}\n'.format('\n- '.join([v.name for v in devel_vars]))
                    ),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Groups([reporting.Groups.INHIBITOR]),
                    reporting.Groups([reporting.Groups.UPGRADE_PROCESS, reporting.Groups.SANITY]),
                    reporting.Remediation(hint=('Invoke leapp without any LEAPP_DEVEL_* environment variables '
                                                'or set LEAPP_UNSUPPORTED=1.'))
                ])

            if experimental:
                reporting.create_report([
                    reporting.Title('Upgrade inhibited due to enabled experimental actors'),
                    reporting.Summary(
                        'One or more enabled experimental actors have been detected.\n'
                        'These actors are unstable or in development and can interfere with the upgrade '
                        'process in unexpected ways. As such, a successful and safe upgrade process cannot be '
                        'guaranteed and the upgrade is unsupported.\n'
                        'You can bypass this error by setting the LEAPP_UNSUPPORTED variable but by doing so, '
                        'you continue at your own risk.\n'
                    ),
                    reporting.Severity(reporting.Severity.HIGH),
                    reporting.Groups([reporting.Groups.INHIBITOR]),
                    reporting.Groups([reporting.Groups.UPGRADE_PROCESS, reporting.Groups.SANITY]),
                    reporting.Remediation(hint=('Invoke leapp without any --whitelist-experimental options '
                                                'or set LEAPP_UNSUPPORTED=1.'))
                ])


# repos/system_upgrade/common/actors/updategrubcore/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.updategrubcore import update_grub_core
from leapp.libraries.common import grub
from leapp.libraries.stdlib import api
from leapp.models import FirmwareFacts, TransactionCompleted
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag, RPMUpgradePhaseTag


class UpdateGrubCore(Actor):
    """
    On legacy (BIOS) systems, GRUB core (located in the gap between the MBR and the
    first partition), does not get automatically updated when GRUB is upgraded.
    """

    name = 'update_grub_core'
    consumes = (TransactionCompleted, FirmwareFacts)
    produces = (Report,)
    tags = (RPMUpgradePhaseTag, IPUWorkflowTag)

    def process(self):
        ff = next(self.consume(FirmwareFacts), None)
        if ff and ff.firmware == 'bios':
            grub_dev = grub.get_grub_device()
            if grub_dev:
                update_grub_core(grub_dev)
            else:
                api.current_logger().warning('Leapp could not detect GRUB on {}'.format(grub_dev))


# repos/system_upgrade/common/actors/usedrepositoriesscanner/actor.py

from leapp.actors import Actor
from leapp.models import (
    InstalledRedHatSignedRPM,
    RepositoriesFacts,
    UsedRepositories,
    UsedRepository
)
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class UsedRepositoriesScanner(Actor):
    """
    Scan used enabled repositories

    Based on lists of installed RPM packages and enabled RPM repositories, check which packages
    were installed from each repository.
    """

    name = 'used_repository_scanner'
    consumes = (InstalledRedHatSignedRPM, RepositoriesFacts)
    produces = (UsedRepositories,)
    tags = (IPUWorkflowTag, FactsPhaseTag)

    def process(self):
        enabled_repos = []
        for repos in self.consume(RepositoriesFacts):
            for repo_file in repos.repositories:
                for repo in repo_file.data:
                    if repo.enabled:
                        enabled_repos.append(repo.repoid)

        installed_pkgs = []
        for rpm_pkgs in self.consume(InstalledRedHatSignedRPM):
            installed_pkgs.extend(rpm_pkgs.items)

        used_repos = {}
        for pkg in installed_pkgs:
            if pkg.repository in enabled_repos:
                used_repos.setdefault(pkg.repository, []).append(pkg.name)

        result = UsedRepositories()
        for repo, pkgs in used_repos.items():
            result.repositories.append(UsedRepository(repository=repo, packages=pkgs))
        self.produce(result)


# repos/system_upgrade/common/actors/verifycheckresults/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.verifycheckresults import check
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag, ReportPhaseTag


class VerifyCheckResults(Actor):
    """
    Check all generated results messages and notify user about them.

    A report file containing all messages will be generated, together with log messages displayed
    to the user.
    """

    name = 'verify_check_results'
    consumes = (Report,)
    produces = ()
    tags = (ReportPhaseTag, IPUWorkflowTag)

    def process(self):
        check()


# repos/system_upgrade/common/actors/verifydialogs/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.verifydialogs import check_dialogs
from leapp.models import DialogModel
from leapp.reporting import Report
from leapp.tags import IPUWorkflowTag, ReportPhaseTag


class VerifyDialogs(Actor):
    """
    Check all dialogs and notify that user needs to make some choices.

    Report messages containing all dialogs with questions that need to be answered will be produced.
    """

    name = 'verify_check_results'
    consumes = (DialogModel,)
    produces = (Report,)
    tags = (ReportPhaseTag, IPUWorkflowTag)

    def process(self):
        check_dialogs(inhibit_if_no_userchoice=self.skip_dialogs)


# repos/system_upgrade/common/actors/xfsinfoscanner/actor.py

from leapp.actors import Actor
from leapp.libraries.actor.xfsinfoscanner import scan_xfs
from leapp.models import StorageInfo, XFSPresence
from leapp.tags import FactsPhaseTag, IPUWorkflowTag


class XFSInfoScanner(Actor):
    """
    This actor scans all mounted mountpoints for XFS information

    The actor will check each mountpoint reported in the StorageInfo message, if the mountpoint is a partition with XFS
    using ftype = 0. The actor will produce a message with the findings.
    It will contain a list of all XFS mountpoints with ftype = 0 so that those mountpoints can be handled appropriately
    for the overlayfs that is going to be created.
    """

    name = 'xfs_info_scanner'
    consumes = (StorageInfo,)
    produces = (XFSPresence,)
    tags = (FactsPhaseTag, IPUWorkflowTag,)

    def process(self):
        scan_xfs()


# repos/system_upgrade/common/actors/missinggpgkeysinhibitor/tests/component_test_missinggpgkey.py
import pytest
from six.moves.urllib.error import URLError

from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.actor.missinggpgkey import _pubkeys_from_rpms, process
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked, produce_mocked
from leapp.libraries.stdlib import api
from leapp.models import (
    InstalledRPM,
    Report,
    RepositoriesFacts,
    RepositoryData,
    RepositoryFile,
    RPM,
    TargetUserSpaceInfo,
    TMPTargetRepositoriesFacts,
    UsedTargetRepositories,
    UsedTargetRepository
)
from leapp.utils.deprecation import suppress_deprecation

# Note, that this is not a real component test as described in the documentation,
# but basically unit test calling the "main" function process() to simulate the
# whole process as I was initially advised not to use these component tests.


def _get_test_installedrpm_no_my_key():
    return [
        RPM(
            name='rpm',
            version='4.16.1.3',
            release='17.el9',
            epoch='0',
            packager='Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>',
            arch='x86_64',
            pgpsig='RSA/SHA256, Mon 08 Aug 2022 09:10:15 AM UTC, Key ID 199e2f91fd431d51',
            repository='BaseOS',
        ),
        RPM(
            name='gpg-pubkey',
            version='fd431d51',
            release='4ae0493b',
            epoch='0',
            packager='Red Hat, Inc. (release key 2) <security@redhat.com>',
            arch='noarch',
            pgpsig=''
        ),
        RPM(
            name='gpg-pubkey',
            version='5a6340b3',
            release='6229229e',
            epoch='0',
            packager='Red Hat, Inc. (auxiliary key 3) <security@redhat.com>',
            arch='noarch',
            pgpsig=''
        ),
    ]


def _get_test_installedrpm():
    return InstalledRPM(
        items=[
            RPM(
                name='gpg-pubkey',
                version='3228467c',
                release='613798eb',
                epoch='0',
                packager='edora (epel9) <epel@fedoraproject.org>',
                arch='noarch',
                pgpsig=''
            ),
        ] + _get_test_installedrpm_no_my_key(),
    )


def _get_test_targuserspaceinfo(path='nopath'):
    return TargetUserSpaceInfo(
        path=path,
        scratch='',
        mounts='',
    )


def _get_test_usedtargetrepositories_list():
    return [
        UsedTargetRepository(
            repoid='BaseOS',
        ),
        UsedTargetRepository(
            repoid='AppStream',
        ),
        UsedTargetRepository(
            repoid='MyAnotherRepo',
        ),
    ]


def _get_test_usedtargetrepositories():
    return UsedTargetRepositories(
        repos=_get_test_usedtargetrepositories_list()
    )


def _get_test_target_repofile():
    return RepositoryFile(
        file='/etc/yum.repos.d/target_rhel.repo',
        data=[
            RepositoryData(
                repoid='BaseOS',
                name="RHEL BaseOS repository",
                baseurl="/whatever/",
                enabled=True,
                additional_fields='{"gpgkey":"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"}'
            ),
            RepositoryData(
                repoid='AppStream',
                name="RHEL AppStream repository",
                baseurl="/whatever/",
                enabled=True,
                additional_fields='{"gpgkey":"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"}'
            ),
        ],
    )


def _get_test_target_repofile_additional():
    return RepositoryFile(
        file='/etc/yum.repos.d/my_target_rhel.repo',
        data=[
            RepositoryData(
                repoid='MyRepo',
                name="My repository",
                baseurl="/whatever/",
                enabled=False,
            ),
            RepositoryData(
                repoid='MyAnotherRepo',
                name="My another repository",
                baseurl="/whatever/",
                enabled=True,
                additional_fields='{"gpgkey":"file:///etc/pki/rpm-gpg/RPM-GPG-KEY-my-release"}'
            ),
        ],
    )


@suppress_deprecation(TMPTargetRepositoriesFacts)
def _get_test_tmptargetrepositoriesfacts():
    return TMPTargetRepositoriesFacts(
        repositories=[
            _get_test_target_repofile(),
            _get_test_target_repofile_additional(),
        ],
    )


def test_perform_nogpgcheck(monkeypatch):
    """
    Executes the "main" function with the --nogpgcheck commandline switch

    This test should skip any checks and just log a message that no checks were executed
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        envars={'LEAPP_NOGPGCHECK': '1'},
        msgs=[
            _get_test_installedrpm(),
            _get_test_usedtargetrepositories(),
            _get_test_tmptargetrepositoriesfacts(),
        ],
    ))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())

    process()

    assert api.produce.called == 0
    assert len(api.current_logger.warnmsg) == 1
    assert '--nogpgcheck option is used' in api.current_logger.warnmsg[0]


@pytest.mark.parametrize('msgs', [
    [],
    [_get_test_installedrpm],
    [_get_test_usedtargetrepositories],
    [_get_test_tmptargetrepositoriesfacts],
    # These are just incomplete lists of required facts
    [_get_test_installedrpm(), _get_test_usedtargetrepositories()],
    [_get_test_usedtargetrepositories(), _get_test_tmptargetrepositoriesfacts()],
    [_get_test_installedrpm(), _get_test_tmptargetrepositoriesfacts()],
])
def test_perform_missing_facts(monkeypatch, msgs):
    """
    Executes the "main" function with missing required facts

    The missing facts (either RPM information, Target Repositories or their facts) cause
    the StopActorExecutionError excepction. But this should be rare as the required facts
    are clearly defined in the actor interface.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=msgs))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    # TODO: the gpg call should be mocked

    with pytest.raises(StopActorExecutionError):
        process()
    # nothing produced
    assert api.produce.called == 0
    # not skipped by --nogpgcheck
    assert not api.current_logger.warnmsg


@suppress_deprecation(TMPTargetRepositoriesFacts)
def _get_test_tmptargetrepositoriesfacts_partial():
    return [
        _get_test_installedrpm(),
        _get_test_usedtargetrepositories(),
        TMPTargetRepositoriesFacts(
            repositories=[
                _get_test_target_repofile(),
                # missing MyAnotherRepo
            ]
        )
    ]


def _gpg_show_keys_mocked(key_path):
    """
    Get faked output from gpg reading keys.

    This is needed to get away from dependency on the filesystem
    """
    if key_path == '/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release':
        return {
            'stdout': [
                'pub:-:4096:1:199E2F91FD431D51:1256212795:::-:::scSC::::::23::0:',
                'fpr:::::::::567E347AD0044ADE55BA8A5F199E2F91FD431D51:',
                ('uid:-::::1256212795::DC1CAEC7997B3575101BB0FCAAC6191792660D8F::'
                 'Red Hat, Inc. (release key 2) <security@redhat.com>::::::::::0:'),
                'pub:-:4096:1:5054E4A45A6340B3:1646863006:::-:::scSC::::::23::0:',
                'fpr:::::::::7E4624258C406535D56D6F135054E4A45A6340B3:',
                ('uid:-::::1646863006::DA7F68E3872D6E7BDCE05225E7EB5F3ACDD9699F::'
                 'Red Hat, Inc. (auxiliary key 3) <security@redhat.com>::::::::::0:'),
            ],
            'stderr': (),
            'exit_code': 0,
        }
    if key_path == '/etc/pki/rpm-gpg/RPM-GPG-KEY-my-release':  # actually epel9 key
        return {
            'stdout': [
                'pub:-:4096:1:8A3872BF3228467C:1631033579:::-:::escESC::::::23::0:',
                'fpr:::::::::FF8AD1344597106ECE813B918A3872BF3228467C:',
                ('uid:-::::1631033579::3EED52B2BDE50880047DB883C87B0FCAE458D111::'
                 'Fedora (epel9) <epel@fedoraproject.org>::::::::::0:'),
            ],
            'stderr': (),
            'exit_code': 0,
        }

    return {
        'stdout': [
            'pub:-:4096:1:F55AD3FB5323552A:1628617948:::-:::escESC::::::23::0:',
            'fpr:::::::::ACB5EE4E831C74BB7C168D27F55AD3FB5323552A:',
            ('uid:-::::1628617948::4830BB019772421B89ABD0BBE245B89C73BF053F::'
             'Fedora (37) <fedora-37-primary@fedoraproject.org>::::::::::0:'),
        ],
        'stderr': (),
        'exit_code': 0,
    }


def _get_pubkeys_mocked(installed_rpms):
    """
    This skips getting fps from files in container for simplification
    """
    return _pubkeys_from_rpms(installed_rpms)


def test_perform_missing_some_repo_facts(monkeypatch):
    """
    Executes the "main" function with missing repositories facts

    This is misalignment in the provided facts UsedTargetRepositories and TMPTargetRepositoriesFacts,
    where we miss some metadata that are required by the first message.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=_get_test_tmptargetrepositoriesfacts_partial())
    )
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._gpg_show_keys', _gpg_show_keys_mocked)

    with pytest.raises(StopActorExecutionError):
        process()
    assert api.produce.called == 0
    assert reporting.create_report.called == 0


@suppress_deprecation(TMPTargetRepositoriesFacts)
def _get_test_tmptargetrepositoriesfacts_https_unused():
    return [
        _get_test_targuserspaceinfo(),
        _get_test_installedrpm(),
        _get_test_usedtargetrepositories(),
        TMPTargetRepositoriesFacts(
            repositories=[
                _get_test_target_repofile(),
                _get_test_target_repofile_additional(),
                RepositoryFile(
                    file='/etc/yum.repos.d/internet.repo',
                    data=[
                        RepositoryData(
                            repoid='ExternalRepo',
                            name="External repository",
                            baseurl="/whatever/path",
                            enabled=True,
                            additional_fields='{"gpgkey":"https://example.com/rpm-gpg/key.gpg"}',
                        ),
                    ],
                )
            ],
        ),
    ]


def test_perform_https_gpgkey_unused(monkeypatch):
    """
    Executes the "main" function with repositories providing keys over internet

    The external repository is not listed in UsedTargetRepositories so the repository
    is not checked and we should not get any error here.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=_get_test_tmptargetrepositoriesfacts_https_unused()
    ))
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._gpg_show_keys', _gpg_show_keys_mocked)
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._get_pubkeys', _get_pubkeys_mocked)

    process()
    assert not api.current_logger.warnmsg
    # This is the DNFWorkaround
    assert api.produce.called == 1
    assert reporting.create_report.called == 1


@suppress_deprecation(TMPTargetRepositoriesFacts)
def get_test_tmptargetrepositoriesfacts_https():
    return (
        _get_test_targuserspaceinfo(),
        _get_test_installedrpm(),
        UsedTargetRepositories(
            repos=_get_test_usedtargetrepositories_list() + [
                UsedTargetRepository(
                    repoid='ExternalRepo',
                ),
            ]
        ),
        TMPTargetRepositoriesFacts(
            repositories=[
                _get_test_target_repofile(),
                _get_test_target_repofile_additional(),
                RepositoryFile(
                    file='/etc/yum.repos.d/internet.repo',
                    data=[
                        RepositoryData(
                            repoid='ExternalRepo',
                            name="External repository",
                            baseurl="/whatever/path",
                            enabled=True,
                            additional_fields='{"gpgkey":"https://example.com/rpm-gpg/key.gpg"}',
                        ),
                    ],
                )
            ],
        ),
    )


@suppress_deprecation(TMPTargetRepositoriesFacts)
def get_test_tmptargetrepositoriesfacts_ftp():
    return (
        _get_test_targuserspaceinfo(),
        _get_test_installedrpm(),
        UsedTargetRepositories(
            repos=_get_test_usedtargetrepositories_list() + [
                UsedTargetRepository(
                    repoid='ExternalRepo',
                ),
            ]
        ),
        TMPTargetRepositoriesFacts(
            repositories=[
                _get_test_target_repofile(),
                _get_test_target_repofile_additional(),
                RepositoryFile(
                    file='/etc/yum.repos.d/internet.repo',
                    data=[
                        RepositoryData(
                            repoid='ExternalRepo',
                            name="External repository",
                            baseurl="/whatever/path",
                            enabled=True,
                            additional_fields='{"gpgkey":"ftp://example.com/rpm-gpg/key.gpg"}',
                        ),
                    ],
                )
            ],
        ),
    )


def _urlretrive_mocked(url, filename=None, reporthook=None, data=None):
    return filename


def test_perform_https_gpgkey(monkeypatch):
    """
    Executes the "main" function with repositories providing keys over internet

    This produces an report.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=get_test_tmptargetrepositoriesfacts_https())
    )
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._gpg_show_keys', _gpg_show_keys_mocked)
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._get_pubkeys', _get_pubkeys_mocked)
    monkeypatch.setattr('six.moves.urllib.request.urlretrieve', _urlretrive_mocked)

    process()
    # This is the DNFWorkaround
    assert api.produce.called == 1
    assert reporting.create_report.called == 1


def _urlretrive_mocked_urlerror(url, filename=None, reporthook=None, data=None):
    raise URLError('error')


def test_perform_https_gpgkey_urlerror(monkeypatch):
    """
    Executes the "main" function with repositories providing keys over internet

    This results in warning message printed. Other than that, no report is still produced.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=get_test_tmptargetrepositoriesfacts_https())
    )
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._gpg_show_keys', _gpg_show_keys_mocked)
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._get_pubkeys', _get_pubkeys_mocked)
    monkeypatch.setattr('six.moves.urllib.request.urlretrieve', _urlretrive_mocked_urlerror)

    process()
    assert len(api.current_logger.warnmsg) == 1
    assert 'Failed to download the gpgkey https://example.com/rpm-gpg/key.gpg:' in api.current_logger.warnmsg[0]
    # This is the DNFWorkaround
    assert api.produce.called == 1
    assert reporting.create_report.called == 1


def test_perform_ftp_gpgkey(monkeypatch):
    """
    Executes the "main" function with repositories providing keys over internet

    This results in error message printed. Other than that, no report is still produced.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=get_test_tmptargetrepositoriesfacts_ftp())
    )
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._gpg_show_keys', _gpg_show_keys_mocked)
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._get_pubkeys', _get_pubkeys_mocked)

    process()
    assert len(api.current_logger.errmsg) == 1
    assert 'Skipping unknown protocol for gpgkey ftp://example.com/rpm-gpg/key.gpg' in api.current_logger.errmsg[0]
    # This is the DNFWorkaround
    assert api.produce.called == 1
    assert reporting.create_report.called == 1


@suppress_deprecation(TMPTargetRepositoriesFacts)
def get_test_data_missing_key():
    return [
        _get_test_targuserspaceinfo(),
        InstalledRPM(items=_get_test_installedrpm_no_my_key()),
        _get_test_usedtargetrepositories(),
        _get_test_tmptargetrepositoriesfacts(),
    ]


def test_perform_report(monkeypatch):
    """
    Executes the "main" function with missing keys

    This should result in report outlining what key mentioned in target repositories is missing.
    """
    monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(
        msgs=get_test_data_missing_key())
    )
    monkeypatch.setattr(api, 'produce', produce_mocked())
    monkeypatch.setattr(api, 'current_logger', logger_mocked())
    monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._gpg_show_keys', _gpg_show_keys_mocked)
    monkeypatch.setattr('leapp.libraries.actor.missinggpgkey._get_pubkeys', _get_pubkeys_mocked)

    process()
    assert not api.current_logger.warnmsg
    # This is the DNFWorkaround
    assert api.produce.called == 1
    assert reporting.create_report.called == 1
