You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

388 lines
15 KiB

10 months ago
import os
from collections import namedtuple
import pytest
from leapp import reporting
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import repofileutils, rhsm
from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked, logger_mocked
from leapp.libraries.stdlib import api, CalledProcessError
from leapp.models import RepositoryData, RepositoryFile
from leapp.utils.report import is_inhibitor
Repository = namedtuple('Repository', ['repoid', 'file'])
LIST_SEPARATOR = '\n - '
# External commands called by the RHSM library
CMD_RHSM_LIST_CONSUMED = ('subscription-manager', 'list', '--consumed')
CMD_RHSM_STATUS = ('subscription-manager', 'status')
CMD_RHSM_RELEASE = ('subscription-manager', 'release')
CMD_RHSM_LIST_ENABLED_REPOS = ('subscription-manager', 'repos', '--list-enabled')
RHSM_STATUS_OUTPUT_NOSCA = '''
+-------------------------------------------+
System Status Details
+-------------------------------------------+
Overall Status: Current
System Purpose Status: Not Specified
'''
RHSM_STATUS_OUTPUT_SCA = '''
+-------------------------------------------+
System Status Details
+-------------------------------------------+
Overall Status: Current
System Purpose Status: Matched
Content Access Mode is set to Simple Content Access
'''
# Used to simulate realistic output of RHSM, therefore carries more information than `Repository` namedtuple
RHSMRepositoryEntry = namedtuple('RHSMRepositoryEntry', ('id', 'name', 'url', 'enabled')) # For clarity purposes
RHSM_ENABLED_REPOS = [
RHSMRepositoryEntry(
id='rhel-8-for-x86_64-appstream-rpms',
name='Appstream',
url='some_url',
enabled='1'),
RHSMRepositoryEntry(
id='satellite-tools-6.6-for-rhel-8-x86_64-rpms',
name='Satellite',
url='some_url',
enabled='1'),
RHSMRepositoryEntry(
id='rhel-8-for-x86_64-baseos-rpms',
name='Base',
url='some_url',
enabled='1')
]
class IsolatedActionsMocked(object):
def __init__(self, call_stdout=None, raise_err=False):
self.commands_called = []
self.call_return = {'stdout': call_stdout, 'stderr': None}
self.raise_err = raise_err
# A map from called commands to their mocked output
self.mocked_command_call_outputs = dict()
def call(self, cmd, *args, **dummy_kwargs):
self.commands_called.append(cmd)
if self.raise_err:
raise_call_error(cmd)
return self.mocked_command_call_outputs.get(
tuple(cmd), # Cast to tuple, as list is not hashable
self.call_return)
def add_mocked_command_call_with_stdout(self, cmd, stdout):
# We cast `cmd` from list to tuple, as a list cannot be hashed
self.mocked_command_call_outputs[tuple(cmd)] = {
'stdout': stdout,
'stderr': None}
def full_path(self, path):
return path
@pytest.fixture
def actor_mocked(monkeypatch):
"""
Fixture providing a mocked actor that was already used to monkeypatch api.current_actor.
Introduced to reduce repetition inside tests.
"""
actor = CurrentActorMocked()
monkeypatch.setattr(api, 'current_actor', actor)
return actor
@pytest.fixture
def context_mocked():
return IsolatedActionsMocked()
def raise_call_error(args=None, exit_code=1):
raise CalledProcessError(
message='Command {0} failed with exit code {1}.'.format(str(args), exit_code),
command=args,
result={'signal': None, 'exit_code': exit_code, 'pid': 0, 'stdout': 'fake out', 'stderr': 'fake err'}
)
def _gen_repo(repoid):
return RepositoryData(repoid=repoid, name='name {}'.format(repoid))
def _gen_repofile(rfile, data=None):
if data is None:
data = [_gen_repo("{}-{}".format(rfile.split("/")[-1], i)) for i in range(3)]
return RepositoryFile(file=rfile, data=data)
@pytest.mark.parametrize('other_repofiles', [
[],
[_gen_repofile("foo")],
[_gen_repofile("foo"), _gen_repofile("bar")],
])
@pytest.mark.parametrize('rhsm_repofile', [
None,
_gen_repofile(rhsm._DEFAULT_RHSM_REPOFILE, []),
_gen_repofile(rhsm._DEFAULT_RHSM_REPOFILE, [_gen_repo("rh-0")]),
_gen_repofile(rhsm._DEFAULT_RHSM_REPOFILE),
])
def test_get_available_repo_ids(monkeypatch, other_repofiles, rhsm_repofile):
context_mocked = IsolatedActionsMocked()
repos = other_repofiles[:]
if rhsm_repofile:
repos.append(rhsm_repofile)
rhsm_repos = [repo.repoid for repo in rhsm_repofile.data] if rhsm_repofile else []
monkeypatch.setattr(api, 'current_logger', logger_mocked())
monkeypatch.setattr(rhsm, '_inhibit_on_duplicate_repos', lambda x: None)
monkeypatch.setattr(repofileutils, 'get_parsed_repofiles', lambda x: repos)
result = rhsm.get_available_repo_ids(context_mocked)
rhsm_repos.sort()
assert context_mocked.commands_called == [['yum', 'clean', 'all']]
assert result == rhsm_repos
if result:
msg = (
'The following repoids are available through RHSM:{0}{1}'
.format(LIST_SEPARATOR, LIST_SEPARATOR.join(rhsm_repos))
)
assert msg in api.current_logger.infomsg
else:
assert 'There are no repos available through RHSM.' in api.current_logger.infomsg
def test_get_available_repo_ids_error():
context_mocked = IsolatedActionsMocked(raise_err=True)
with pytest.raises(StopActorExecutionError) as err:
rhsm.get_available_repo_ids(context_mocked)
assert 'Unable to use yum' in str(err)
def test_inhibit_on_duplicate_repos(monkeypatch):
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
monkeypatch.setattr(api, 'current_logger', logger_mocked())
repofiles = [
_gen_repofile("foo", [_gen_repo('repoX'), _gen_repo('repoY')]),
_gen_repofile("bar", [_gen_repo('repoX')]),
]
rhsm._inhibit_on_duplicate_repos(repofiles)
dups = ['repoX']
assert ('The following repoids are defined multiple times:{0}{1}'
.format(LIST_SEPARATOR, LIST_SEPARATOR.join(dups))) in api.current_logger.warnmsg
assert reporting.create_report.called == 1
assert is_inhibitor(reporting.create_report.report_fields)
assert reporting.create_report.report_fields['title'] == 'A YUM/DNF repository defined multiple times'
summary = ('The following repositories are defined multiple times:{0}{1}'
.format(LIST_SEPARATOR, LIST_SEPARATOR.join(dups)))
assert summary in reporting.create_report.report_fields['summary']
def test_inhibit_on_duplicate_repos_no_dups(monkeypatch):
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
monkeypatch.setattr(api, 'current_logger', logger_mocked())
rhsm._inhibit_on_duplicate_repos([_gen_repofile("foo")])
assert not api.current_logger.warnmsg
assert reporting.create_report.called == 0
def test_sku_listing(monkeypatch, actor_mocked, context_mocked):
"""Tests whether the rhsm library can obtain used SKUs correctly."""
context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_LIST_CONSUMED, 'SKU: 598339696910')
attached_skus = rhsm.get_attached_skus(context_mocked)
assert_fail_description = 'Some calls to subscription-manager were expected.'
assert context_mocked.commands_called, assert_fail_description
assert_fail_description = 'RHSM command reported 1 SKU, however {0} were detected.'.format(
len(attached_skus)
)
assert len(attached_skus) == 1, assert_fail_description
assert_fail_description = 'The parsed SKU is different than the one contained in the mocked RHSM output.'
assert attached_skus[0] == '598339696910', assert_fail_description
def test_scanrhsminfo_with_skip_rhsm(monkeypatch, context_mocked):
"""Tests whether the scan_rhsm_info respects the LEAPP_NO_RHSM environmental variable."""
mocked_actor = CurrentActorMocked(envars={'LEAPP_NO_RHSM': '1'})
monkeypatch.setattr(api, 'current_actor', mocked_actor)
result = rhsm.scan_rhsm_info(context_mocked)
assert_fail_description = 'No external shell commands should be executed when RHSM is skipped.'
assert not context_mocked.commands_called, assert_fail_description
assert result is None, 'The `scan_rhsm_info` should not provide any output when RHSM is skipped.'
def test_get_release(monkeypatch, actor_mocked, context_mocked):
"""Tests whether the library correctly retrieves release from RHSM."""
context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_RELEASE, 'Release: 7.9')
release = rhsm.get_release(context_mocked)
assert release, 'No release information detected (but valid release info was provided).'
assert release == '7.9', 'Detected release is incorrect.'
def test_get_release_with_release_not_set(monkeypatch, actor_mocked, context_mocked):
"""Tests whether the library does not retrieve release information when the release is not set."""
# Test whether no release is detected correctly too
context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_RELEASE, 'Release not set')
release = rhsm.get_release(context_mocked)
fail_description = 'The release information was obtained, even if "No release set" was repored by rhsm.'
assert not release, fail_description
def test_is_manifest_sca_on_nonsca_system(monkeypatch, actor_mocked, context_mocked):
"""Tests whether the library obtains the SCA information correctly from a non-SCA system."""
context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_STATUS, RHSM_STATUS_OUTPUT_NOSCA)
is_sca = rhsm.is_manifest_sca(context_mocked)
assert not is_sca, 'SCA was detected on a non-SCA system.'
def test_is_manifest_sca_on_sca_system(monkeypatch, actor_mocked, context_mocked):
"""Tests whether the library obtains the SCA information from SCA system correctly."""
context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_STATUS, RHSM_STATUS_OUTPUT_SCA)
is_sca = rhsm.is_manifest_sca(context_mocked)
assert is_sca, 'Failed to detected SCA on a SCA system.'
def test_get_enabled_repo_ids(monkeypatch, actor_mocked, context_mocked):
"""Tests whether the library retrieves correct information about enabled repositories."""
# Prepare the (realistic) RHSM output
rhsm_list_enabled_output = '''
+----------------------------------------------------------+
Available Repositories in /etc/yum.repos.d/redhat.repo
+----------------------------------------------------------+
'''
for enabled_repository in RHSM_ENABLED_REPOS:
rhsm_output_fragment = 'Repo ID: {0}\n'.format(enabled_repository.id)
rhsm_output_fragment += 'Repo Name: {0}\n'.format(enabled_repository.name)
rhsm_output_fragment += 'Repo URL: {0}\n'.format(enabled_repository.url)
rhsm_output_fragment += 'Enabled: {0}\n'.format(enabled_repository.enabled)
rhsm_output_fragment += '\n'
rhsm_list_enabled_output += rhsm_output_fragment
context_mocked.add_mocked_command_call_with_stdout(CMD_RHSM_LIST_ENABLED_REPOS, rhsm_list_enabled_output)
enabled_repo_ids = rhsm.get_enabled_repo_ids(context_mocked)
fail_description = 'Failed to detected enabled repositories on the system.'
assert len(enabled_repo_ids) == 3, fail_description
fail_description = 'Failed to retrieve repository ID provided in the RHSM output.'
for enabled_repository in RHSM_ENABLED_REPOS:
assert enabled_repository.id in enabled_repo_ids, fail_description
def test_get_existing_product_certificates(monkeypatch, actor_mocked, context_mocked):
"""Verifies that the library is able to correctly retrieve existing product certificates."""
CERT_DIRS_LAYOUT = {
'/etc/pki/product': ['cert1', 'cert2'],
'/etc/pki/product-default': ['cert3']
}
def mocked_isdir(path):
if path in CERT_DIRS_LAYOUT:
return True
err_message = 'RHSM library should not gather info about additional dirs (attempted to isdir: {0}).'
raise ValueError(err_message.format(path))
def mocked_listdir(path):
if path in CERT_DIRS_LAYOUT:
return CERT_DIRS_LAYOUT[path]
err_message = 'RHSM library should not listdir additional dirs (attempted to listdir: {0}).'
raise ValueError(err_message.format(path))
def mocked_isfile(path):
if path in CERT_DIRS_LAYOUT:
# The certificate directories are not files
return False
basename = os.path.basename(path)
dirname = os.path.dirname(path)
if dirname in CERT_DIRS_LAYOUT:
return basename in CERT_DIRS_LAYOUT[dirname]
err_message = 'RHSM library should not isfile additional paths (attempted to isfile: {0}).'
raise ValueError(err_message.format(path))
monkeypatch.setattr(rhsm.os.path, 'isdir', mocked_isdir)
monkeypatch.setattr(rhsm.os, 'listdir', mocked_listdir)
monkeypatch.setattr(rhsm.os.path, 'isfile', mocked_isfile)
existing_product_certificates = rhsm.get_existing_product_certificates(context_mocked)
fail_description = 'Retrieved different number of certificates than expected.'
assert len(existing_product_certificates) == 3, fail_description
fail_description_bad_dir = 'Found certificate in unexpected path: {0}'
fail_description_bad_cert_file = 'Found certificate file that was not provided by mocked output: {0}'
for certificate_path in existing_product_certificates:
dirname = os.path.dirname(certificate_path)
basename = os.path.basename(certificate_path)
assert dirname in CERT_DIRS_LAYOUT, fail_description_bad_dir.format(certificate_path)
assert basename in CERT_DIRS_LAYOUT[dirname], fail_description_bad_cert_file.format(certificate_path)
def test_get_existing_product_certificates_missing_cert_directory(monkeypatch, actor_mocked, context_mocked):
"""Tests whether the library is able to retrieve certificates even if /etc/pki/product is missing."""
def mocked_isdir(path):
if path == '/etc/pki/product':
return False # Directory is missing
if path == '/etc/pki/product-default':
return True
err_msg = 'Tried to isdir a path that is not a part of the mocked paths. Path: {0}'
raise ValueError(err_msg.format(path))
def mocked_isfile(path):
if path == '/etc/pki/product-default/cert':
return True
err_msg = 'Tried to use isfile on a path that is not a part of the mocked paths. Path: {0}'
raise ValueError(err_msg.format(path))
def mocked_listdir(path):
if path == '/etc/pki/product-default':
return ['cert']
err_msg = 'Tried to use listdir on a path that is not a part of the mocked paths. Path: {0}'
raise ValueError(err_msg.format(path))
monkeypatch.setattr(rhsm.os.path, 'isdir', mocked_isdir)
monkeypatch.setattr(rhsm.os, 'listdir', mocked_listdir)
monkeypatch.setattr(rhsm.os.path, 'isfile', mocked_isfile)
existing_product_certificates = rhsm.get_existing_product_certificates(context_mocked)
fail_description = 'Library identified more certificates than there are in mocked outputs.'
assert len(existing_product_certificates) == 1, fail_description
fail_description = 'Library failed to identify certificate from mocked outputs.'
assert existing_product_certificates[0] == '/etc/pki/product-default/cert', fail_description