diff --git a/repos/system_upgrade/common/actors/checkldconf/actor.py b/repos/system_upgrade/common/actors/checkldconf/actor.py new file mode 100644 index 0000000000..ef1d0c57d7 --- /dev/null +++ b/repos/system_upgrade/common/actors/checkldconf/actor.py @@ -0,0 +1,23 @@ +from leapp.actors import Actor +from leapp.libraries.actor.checkldsoconfiguration import check_ld_so_configuration +from leapp.models import InstalledRedHatSignedRPM, Report +from leapp.tags import ChecksPhaseTag, IPUWorkflowTag + + +class CheckLdSoConfiguration(Actor): + """ + Check for customization of ld.so configuration + + The ld.so configuration files are used to overwrite standard library links + in order to use different custom libraries. The in-place upgrade does not + support customization of this configuration by user. This actor inhibits the + upgrade upon detecting such customization. + """ + + name = 'check_ld_so_configuration' + consumes = (InstalledRedHatSignedRPM,) + produces = (Report,) + tags = (ChecksPhaseTag, IPUWorkflowTag) + + def process(self): + check_ld_so_configuration() diff --git a/repos/system_upgrade/common/actors/checkldconf/libraries/checkldsoconfiguration.py b/repos/system_upgrade/common/actors/checkldconf/libraries/checkldsoconfiguration.py new file mode 100644 index 0000000000..8286fb62f2 --- /dev/null +++ b/repos/system_upgrade/common/actors/checkldconf/libraries/checkldsoconfiguration.py @@ -0,0 +1,98 @@ +import glob +import os + +from leapp import reporting +from leapp.libraries.common.rpms import has_package +from leapp.libraries.stdlib import api, CalledProcessError, run +from leapp.models import InstalledRedHatSignedRPM + +LD_SO_CONF_DIR = '/etc/ld.so.conf.d' +LD_SO_CONF_MAIN = '/etc/ld.so.conf' +LD_SO_CONF_DEFAULT_INCLUDE = 'ld.so.conf.d/*.conf' +LIST_FORMAT_PREFIX = '\n - ' + + +def _read_file(file_path): + with open(file_path, 'r') as fd: + return fd.readlines() + + +def _report_custom_ld_so_configuration(summary): + reporting.create_report([ + reporting.Title( + 'Third party libraries linked with ld.so.conf are not supported for the in-place upgrade.' + ), + reporting.Summary(summary), + reporting.Remediation('Remove the custom ld.so configuration.'), + reporting.RelatedResource('file', '/etc/ld.so.conf'), + reporting.RelatedResource('directory', '/etc/ld.so.conf.d'), + reporting.Severity(reporting.Severity.HIGH), + reporting.Groups([reporting.Groups.OS_FACTS, reporting.Groups.INHIBITOR]), + ]) + + +def _is_included_ld_so_config_custom(config_path): + if not os.path.isfile(config_path): + return False + + is_custom = False + try: + package_name = run(['rpm', '-qf', '--queryformat', '%{NAME}', config_path])['stdout'] + is_custom = not has_package(InstalledRedHatSignedRPM, package_name) + except CalledProcessError: + is_custom = True + api.current_logger().debug('The following config file is{}considered a custom ' + 'ld.so configuration: {}'.format(' ' if is_custom else ' NOT ', config_path)) + return is_custom + + +def _parse_main_ld_so_config(): + config = _read_file(LD_SO_CONF_MAIN) + + included_configs = [] + other_lines = [] + for line in config: + line = line.strip() + if line.startswith('include'): + cfg_glob = line.split(' ', 1)[1].strip() + cfg_glob = os.path.join('/etc', cfg_glob) if not os.path.isabs(cfg_glob) else cfg_glob + included_configs.append(cfg_glob) + elif line: + other_lines.append(line) + + return included_configs, other_lines + + +def check_ld_so_configuration(): + included_configs, other_lines = _parse_main_ld_so_config() + summary = '' + + if other_lines: + # The main ld.so config file is expected to only include additional configs that are owned by a package + summary += ('The /etc/ld.so.conf file has unexpected contents:' + '{}{}'.format(LIST_FORMAT_PREFIX, + LIST_FORMAT_PREFIX.join(other_lines))) + + is_default_include_present = '/etc/' + LD_SO_CONF_DEFAULT_INCLUDE in included_configs + if not is_default_include_present: + api.current_logger().debug('The default include "' + LD_SO_CONF_DEFAULT_INCLUDE + + '" is not present in the ' + LD_SO_CONF_MAIN + ' file.') + + if is_default_include_present and len(included_configs) != 1: + # The additional included configs will most likely be created manually by the user + # and therefore will get flagged as custom in the next part of this function + api.current_logger().debug('The default include "' + LD_SO_CONF_DEFAULT_INCLUDE + + '" is not the only include in the ' + LD_SO_CONF_MAIN + ' file.') + + # Detect custom ld configs from the includes in the main ld.so.conf + custom_ld_configs = [] + for cfg_glob in included_configs: + custom_ld_configs += [cfg for cfg in glob.glob(cfg_glob) if _is_included_ld_so_config_custom(cfg)] + + if custom_ld_configs: + summary += ('\nThe following config files were marked as unsupported:' + '{}{}'.format(LIST_FORMAT_PREFIX, + LIST_FORMAT_PREFIX.join(custom_ld_configs))) + + if summary: + _report_custom_ld_so_configuration(summary) diff --git a/repos/system_upgrade/common/actors/checkldconf/tests/test_checkldsoconfiguration.py b/repos/system_upgrade/common/actors/checkldconf/tests/test_checkldsoconfiguration.py new file mode 100644 index 0000000000..cef9dc675b --- /dev/null +++ b/repos/system_upgrade/common/actors/checkldconf/tests/test_checkldsoconfiguration.py @@ -0,0 +1,125 @@ +import glob +import os + +import pytest + +from leapp import reporting +from leapp.libraries.actor import checkldsoconfiguration +from leapp.libraries.common.testutils import create_report_mocked +from leapp.libraries.stdlib import CalledProcessError +from leapp.models import InstalledRedHatSignedRPM + +INCLUDED_CONFIGS_GLOB_DICT_1 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf', + '/etc/ld.so.conf.d/mariadb-x86_64.conf', + '/etc/ld.so.conf.d/bind-export-x86_64.conf']} + +INCLUDED_CONFIGS_GLOB_DICT_2 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf', + '/etc/ld.so.conf.d/mariadb-x86_64.conf', + '/etc/ld.so.conf.d/bind-export-x86_64.conf', + '/etc/ld.so.conf.d/custom1.conf', + '/etc/ld.so.conf.d/custom2.conf']} + +INCLUDED_CONFIGS_GLOB_DICT_3 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf', + '/etc/ld.so.conf.d/custom1.conf', + '/etc/ld.so.conf.d/mariadb-x86_64.conf', + '/etc/ld.so.conf.d/bind-export-x86_64.conf', + '/etc/ld.so.conf.d/custom2.conf'], + '/custom/path/*.conf': ['/custom/path/custom1.conf', + '/custom/path/custom2.conf']} + + +@pytest.mark.parametrize(('included_configs_glob_dict', 'other_lines', 'custom_configs'), + [ + (INCLUDED_CONFIGS_GLOB_DICT_1, [], []), + (INCLUDED_CONFIGS_GLOB_DICT_1, ['/custom/path.lib'], []), + (INCLUDED_CONFIGS_GLOB_DICT_2, [], ['/etc/ld.so.conf.d/custom1.conf', + '/etc/ld.so.conf.d/custom2.conf']), + (INCLUDED_CONFIGS_GLOB_DICT_3, ['/custom/path.lib'], ['/etc/ld.so.conf.d/custom1.conf', + '/etc/ld.so.conf.d/custom2.conf' + '/custom/path/custom1.conf', + '/custom/path/custom2.conf']), + ]) +def test_check_ld_so_configuration(monkeypatch, included_configs_glob_dict, other_lines, custom_configs): + monkeypatch.setattr(reporting, 'create_report', create_report_mocked()) + monkeypatch.setattr(glob, 'glob', lambda glob: included_configs_glob_dict[glob]) + monkeypatch.setattr(checkldsoconfiguration, '_is_included_ld_so_config_custom', + lambda config: config in custom_configs) + monkeypatch.setattr(checkldsoconfiguration, '_parse_main_ld_so_config', + lambda: (included_configs_glob_dict.keys(), other_lines)) + + checkldsoconfiguration.check_ld_so_configuration() + + report_expected = custom_configs or other_lines + if not report_expected: + assert reporting.create_report.called == 0 + return + + assert reporting.create_report.called == 1 + assert 'ld.so.conf' in reporting.create_report.reports[0]['title'] + summary = reporting.create_report.reports[0]['summary'] + + all_configs = [] + for configs in included_configs_glob_dict.values(): + all_configs += configs + + if custom_configs: + assert 'The following config files were marked as unsupported:' in summary + + for config in all_configs: + assert (config in custom_configs) == (config in summary) + + if other_lines: + assert 'The /etc/ld.so.conf file has unexpected contents' in summary + + for other_line in other_lines: + assert other_line in summary + + +@pytest.mark.parametrize(('config_contents', 'included_config_paths', 'other_lines'), + [ + (['include ld.so.conf.d/*.conf\n'], + ['/etc/ld.so.conf.d/*.conf'], []), + (['include ld.so.conf.d/*.conf\n', '\n', '/custom/path.lib\n'], + ['/etc/ld.so.conf.d/*.conf'], ['/custom/path.lib']), + (['include ld.so.conf.d/*.conf\n', 'include /custom/path.conf\n'], + ['/etc/ld.so.conf.d/*.conf', '/custom/path.conf'], []), + ([' \n'], + [], []) + ]) +def test_parse_main_ld_so_config(monkeypatch, config_contents, included_config_paths, other_lines): + def mocked_read_file(path): + assert path == checkldsoconfiguration.LD_SO_CONF_MAIN + return config_contents + + monkeypatch.setattr(checkldsoconfiguration, '_read_file', mocked_read_file) + + _included_config_paths, _other_lines = checkldsoconfiguration._parse_main_ld_so_config() + + assert _included_config_paths == included_config_paths + assert _other_lines == other_lines + + +@pytest.mark.parametrize(('config_path', 'run_result', 'package_exists', 'is_custom'), + [ + ('/etc/ld.so.conf.d/dyninst-x86_64.conf', 'dyninst', True, False), + ('/etc/ld.so.conf.d/somelib.conf', CalledProcessError, False, True), + ('/etc/custom/custom.conf', 'custom', False, True) + ]) +def test_is_included_ld_so_config_custom(monkeypatch, config_path, run_result, package_exists, is_custom): + def mocked_run(command): + assert config_path in command + if run_result and not isinstance(run_result, str): + raise CalledProcessError("message", ["command"], "result") + return {'stdout': run_result} + + def mocked_has_package(model, package_name): + assert model is InstalledRedHatSignedRPM + assert package_name == run_result + return package_exists + + monkeypatch.setattr(checkldsoconfiguration, 'run', mocked_run) + monkeypatch.setattr(checkldsoconfiguration, 'has_package', mocked_has_package) + monkeypatch.setattr(os.path, 'isfile', lambda _: True) + + result = checkldsoconfiguration._is_included_ld_so_config_custom(config_path) + assert result == is_custom