diff --git a/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/actor.py b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/actor.py new file mode 100644 index 0000000000..6671eef43f --- /dev/null +++ b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/actor.py @@ -0,0 +1,22 @@ +from leapp.actors import Actor +from leapp.libraries.actor.checkdynamiclinkerconfiguration import check_dynamic_linker_configuration +from leapp.models import DynamicLinkerConfiguration, Report +from leapp.tags import ChecksPhaseTag, IPUWorkflowTag + + +class CheckDynamicLinkerConfiguration(Actor): + """ + Check for customization of dynamic linker configuration. + + The in-place upgrade could potentionally be impacted in a negative way due + to the customization of dynamic linker configuration by user. This actor creates high + severity report upon detecting such customization. + """ + + name = 'check_dynamic_linker_configuration' + consumes = (DynamicLinkerConfiguration,) + produces = (Report,) + tags = (ChecksPhaseTag, IPUWorkflowTag) + + def process(self): + check_dynamic_linker_configuration() diff --git a/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/libraries/checkdynamiclinkerconfiguration.py b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/libraries/checkdynamiclinkerconfiguration.py new file mode 100644 index 0000000000..9ead892e88 --- /dev/null +++ b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/libraries/checkdynamiclinkerconfiguration.py @@ -0,0 +1,79 @@ +from leapp import reporting +from leapp.libraries.stdlib import api +from leapp.models import DynamicLinkerConfiguration + +LD_SO_CONF_DIR = '/etc/ld.so.conf.d' +LD_SO_CONF_MAIN = '/etc/ld.so.conf' +LD_LIBRARY_PATH_VAR = 'LD_LIBRARY_PATH' +LD_PRELOAD_VAR = 'LD_PRELOAD' +FMT_LIST_SEPARATOR_1 = '\n- ' +FMT_LIST_SEPARATOR_2 = '\n - ' + + +def _report_custom_dynamic_linker_configuration(summary): + reporting.create_report([ + reporting.Title( + 'Detected customized configuration for dynamic linker.' + ), + reporting.Summary(summary), + reporting.Remediation(hint=('Remove or revert the custom dynamic linker configurations and apply the changes ' + 'using the ldconfig command. In case of possible active software collections we ' + 'suggest disabling them persistently.')), + 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]), + ]) + + +def check_dynamic_linker_configuration(): + configuration = next(api.consume(DynamicLinkerConfiguration), None) + if not configuration: + return + + custom_configurations = '' + if configuration.main_config.modified: + custom_configurations += ( + '{}The {} file has unexpected contents:{}{}' + .format(FMT_LIST_SEPARATOR_1, LD_SO_CONF_MAIN, + FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(configuration.main_config.modified_lines)) + ) + + custom_configs = [] + for config in configuration.included_configs: + if config.modified: + custom_configs.append(config.path) + + if custom_configs: + custom_configurations += ( + '{}The following drop in config files were marked as custom:{}{}' + .format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2, FMT_LIST_SEPARATOR_2.join(custom_configs)) + ) + + if configuration.used_variables: + custom_configurations += ( + '{}The following variables contain unexpected dynamic linker configuration:{}{}' + .format(FMT_LIST_SEPARATOR_1, FMT_LIST_SEPARATOR_2, + FMT_LIST_SEPARATOR_2.join(configuration.used_variables)) + ) + + if custom_configurations: + summary = ( + 'Custom configurations to the dynamic linker could potentially impact ' + 'the upgrade in a negative way. The custom configuration includes ' + 'modifications to {main_conf}, custom or modified drop in config ' + 'files in the {conf_dir} directory and additional entries in the ' + '{ldlib_envar} or {ldpre_envar} variables. These modifications ' + 'configure the dynamic linker to use different libraries that might ' + 'not be provided by Red Hat products or might not be present during ' + 'the whole upgrade process. The following custom configurations ' + 'were detected by leapp:{cust_configs}' + .format( + main_conf=LD_SO_CONF_MAIN, + conf_dir=LD_SO_CONF_DIR, + ldlib_envar=LD_LIBRARY_PATH_VAR, + ldpre_envar=LD_PRELOAD_VAR, + cust_configs=custom_configurations + ) + ) + _report_custom_dynamic_linker_configuration(summary) diff --git a/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/tests/test_checkdynamiclinkerconfiguration.py b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/tests/test_checkdynamiclinkerconfiguration.py new file mode 100644 index 0000000000..d640f0c5bc --- /dev/null +++ b/repos/system_upgrade/common/actors/checkdynamiclinkerconfiguration/tests/test_checkdynamiclinkerconfiguration.py @@ -0,0 +1,65 @@ +import pytest + +from leapp import reporting +from leapp.libraries.actor.checkdynamiclinkerconfiguration import ( + check_dynamic_linker_configuration, + LD_LIBRARY_PATH_VAR, + LD_PRELOAD_VAR +) +from leapp.libraries.common.testutils import create_report_mocked, CurrentActorMocked +from leapp.libraries.stdlib import api +from leapp.models import DynamicLinkerConfiguration, LDConfigFile, MainLDConfigFile + +INCLUDED_CONFIG_PATHS = ['/etc/ld.so.conf.d/dyninst-x86_64.conf', + '/etc/ld.so.conf.d/mariadb-x86_64.conf', + '/custom/path/custom1.conf'] + + +@pytest.mark.parametrize(('included_configs_modifications', 'used_variables', 'modified_lines'), + [ + ([False, False, False], [], []), + ([True, True, True], [], []), + ([False, False, False], [LD_LIBRARY_PATH_VAR], []), + ([False, False, False], [], ['modified line 1', 'midified line 2']), + ([True, False, True], [LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR], ['modified line']), + ]) +def test_check_ld_so_configuration(monkeypatch, included_configs_modifications, used_variables, modified_lines): + assert len(INCLUDED_CONFIG_PATHS) == len(included_configs_modifications) + + main_config = MainLDConfigFile(path="/etc/ld.so.conf", modified=any(modified_lines), modified_lines=modified_lines) + included_configs = [] + for path, modified in zip(INCLUDED_CONFIG_PATHS, included_configs_modifications): + included_configs.append(LDConfigFile(path=path, modified=modified)) + + configuration = DynamicLinkerConfiguration(main_config=main_config, + included_configs=included_configs, + used_variables=used_variables) + + monkeypatch.setattr(api, 'current_actor', CurrentActorMocked(msgs=[configuration])) + monkeypatch.setattr(reporting, 'create_report', create_report_mocked()) + + check_dynamic_linker_configuration() + + report_expected = any(included_configs_modifications) or modified_lines or used_variables + if not report_expected: + assert reporting.create_report.called == 0 + return + + assert reporting.create_report.called == 1 + assert 'configuration for dynamic linker' in reporting.create_report.reports[0]['title'] + summary = reporting.create_report.reports[0]['summary'] + + if any(included_configs_modifications): + assert 'The following drop in config files were marked as custom:' in summary + for config, modified in zip(INCLUDED_CONFIG_PATHS, included_configs_modifications): + assert modified == (config in summary) + + if modified_lines: + assert 'The /etc/ld.so.conf file has unexpected contents' in summary + for line in modified_lines: + assert line in summary + + if used_variables: + assert 'The following variables contain unexpected dynamic linker configuration:' in summary + for var in used_variables: + assert '- {}'.format(var) in summary diff --git a/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/actor.py b/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/actor.py new file mode 100644 index 0000000000..11283cd0c8 --- /dev/null +++ b/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/actor.py @@ -0,0 +1,23 @@ +from leapp.actors import Actor +from leapp.libraries.actor.scandynamiclinkerconfiguration import scan_dynamic_linker_configuration +from leapp.models import DynamicLinkerConfiguration, InstalledRedHatSignedRPM +from leapp.tags import FactsPhaseTag, IPUWorkflowTag + + +class ScanDynamicLinkerConfiguration(Actor): + """ + Scan the dynamic linker configuration and find modifications. + + The dynamic linker configuration files can be used to replace standard libraries + with different custom libraries. The in-place upgrade does not support customization + of this configuration by user. This actor produces information about detected + modifications. + """ + + name = 'scan_dynamic_linker_configuration' + consumes = (InstalledRedHatSignedRPM,) + produces = (DynamicLinkerConfiguration,) + tags = (FactsPhaseTag, IPUWorkflowTag) + + def process(self): + scan_dynamic_linker_configuration() diff --git a/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/libraries/scandynamiclinkerconfiguration.py b/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/libraries/scandynamiclinkerconfiguration.py new file mode 100644 index 0000000000..1a6ab6a217 --- /dev/null +++ b/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/libraries/scandynamiclinkerconfiguration.py @@ -0,0 +1,117 @@ +import glob +import os + +from leapp.libraries.common.rpms import has_package +from leapp.libraries.stdlib import api, CalledProcessError, run +from leapp.models import DynamicLinkerConfiguration, InstalledRedHatSignedRPM, LDConfigFile, MainLDConfigFile + +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' +LD_SO_CONF_COMMENT_PREFIX = '#' +LD_LIBRARY_PATH_VAR = 'LD_LIBRARY_PATH' +LD_PRELOAD_VAR = 'LD_PRELOAD' + + +def _read_file(file_path): + with open(file_path, 'r') as fd: + return fd.readlines() + + +def _is_modified(config_path): + """ Decide if the configuration file was modified based on the package it belongs to. """ + result = run(['rpm', '-Vf', config_path], checked=False) + if not result['exit_code']: + return False + modification_flags = result['stdout'].split(' ', 1)[0] + # The file is considered modified only when the checksum does not match + return '5' in modification_flags + + +def _is_included_config_custom(config_path): + if not os.path.isfile(config_path): + return False + + # Check if the config file has any lines that have an effect on dynamic linker configuration + has_effective_line = False + for line in _read_file(config_path): + line = line.strip() + if line and not line.startswith(LD_SO_CONF_COMMENT_PREFIX): + has_effective_line = True + break + + if not has_effective_line: + return False + + is_custom = False + try: + package_name = run(['rpm', '-qf', '--queryformat', '%{NAME}', config_path])['stdout'] + is_custom = not has_package(InstalledRedHatSignedRPM, package_name) or _is_modified(config_path) + except CalledProcessError: + is_custom = True + + return is_custom + + +def _parse_main_config(): + """ + Extracts included configs from the main dynamic linker configuration file (/etc/ld.so.conf) + along with lines that are likely custom. The lines considered custom are simply those that are + not includes. + + :returns: tuple containing all the included files and lines considered custom + :rtype: tuple(list, list) + """ + 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 and not line.startswith(LD_SO_CONF_COMMENT_PREFIX): + other_lines.append(line) + + return included_configs, other_lines + + +def scan_dynamic_linker_configuration(): + included_configs, other_lines = _parse_main_config() + + 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 "{}" is not present in ' + 'the {} file.'.format(LD_SO_CONF_DEFAULT_INCLUDE, LD_SO_CONF_MAIN)) + + 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 "{}" is not the only include in ' + 'the {} file.'.format(LD_SO_CONF_DEFAULT_INCLUDE, LD_SO_CONF_MAIN)) + + main_config_file = MainLDConfigFile(path=LD_SO_CONF_MAIN, modified=any(other_lines), modified_lines=other_lines) + + # Expand the config paths from globs and ensure uniqueness of resulting paths + config_paths = set() + for cfg_glob in included_configs: + for cfg in glob.glob(cfg_glob): + config_paths.add(cfg) + + included_config_files = [] + for config_path in config_paths: + config_file = LDConfigFile(path=config_path, modified=_is_included_config_custom(config_path)) + included_config_files.append(config_file) + + # Check if dynamic linker variables used for specifying custom libraries are set + variables = [LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR] + used_variables = [var for var in variables if os.getenv(var, None)] + + configuration = DynamicLinkerConfiguration(main_config=main_config_file, + included_configs=included_config_files, + used_variables=used_variables) + + if other_lines or any([config.modified for config in included_config_files]) or used_variables: + api.produce(configuration) diff --git a/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/tests/test_scandynamiclinkerconfiguration.py b/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/tests/test_scandynamiclinkerconfiguration.py new file mode 100644 index 0000000000..2114495113 --- /dev/null +++ b/repos/system_upgrade/common/actors/scandynamiclinkerconfiguration/tests/test_scandynamiclinkerconfiguration.py @@ -0,0 +1,181 @@ +import glob +import os + +import pytest + +from leapp import reporting +from leapp.libraries.actor import scandynamiclinkerconfiguration +from leapp.libraries.common.testutils import produce_mocked +from leapp.libraries.stdlib import api, 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', 'used_variables'), + [ + (INCLUDED_CONFIGS_GLOB_DICT_1, [], [], []), + (INCLUDED_CONFIGS_GLOB_DICT_1, ['/custom/path.lib'], [], []), + (INCLUDED_CONFIGS_GLOB_DICT_1, [], [], ['LD_LIBRARY_PATH']), + (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_scan_dynamic_linker_configuration(monkeypatch, included_configs_glob_dict, other_lines, + custom_configs, used_variables): + monkeypatch.setattr(scandynamiclinkerconfiguration, '_parse_main_config', + lambda: (included_configs_glob_dict.keys(), other_lines)) + monkeypatch.setattr(glob, 'glob', lambda glob: included_configs_glob_dict[glob]) + monkeypatch.setattr(scandynamiclinkerconfiguration, '_is_included_config_custom', + lambda config: config in custom_configs) + monkeypatch.setattr(api, 'produce', produce_mocked()) + + for var in used_variables: + monkeypatch.setenv(var, '/some/path') + + scandynamiclinkerconfiguration.scan_dynamic_linker_configuration() + + produce_expected = custom_configs or other_lines or used_variables + if not produce_expected: + assert not api.produce.called + return + + assert api.produce.called == 1 + + configuration = api.produce.model_instances[0] + + all_configs = [] + for configs in included_configs_glob_dict.values(): + all_configs += configs + + assert len(all_configs) == len(configuration.included_configs) + for config in configuration.included_configs: + if config.path in custom_configs: + assert config.modified + + assert configuration.main_config.path == scandynamiclinkerconfiguration.LD_SO_CONF_MAIN + if other_lines: + assert configuration.main_config.modified + assert configuration.main_config.modified_lines == other_lines + + if used_variables: + assert configuration.used_variables == used_variables + + +@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', '#comment'], + ['/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'], []), + (['include ld.so.conf.d/*.conf\n', '#include /custom/path.conf\n', '#/custom/path.conf\n'], + ['/etc/ld.so.conf.d/*.conf'], []), + ([' \n'], + [], []) + ]) +def test_parse_main_config(monkeypatch, config_contents, included_config_paths, other_lines): + def mocked_read_file(path): + assert path == scandynamiclinkerconfiguration.LD_SO_CONF_MAIN + return config_contents + + monkeypatch.setattr(scandynamiclinkerconfiguration, '_read_file', mocked_read_file) + + _included_config_paths, _other_lines = scandynamiclinkerconfiguration._parse_main_config() + + assert _included_config_paths == included_config_paths + assert _other_lines == other_lines + + +@pytest.mark.parametrize(('config_path', 'run_result', 'is_modified'), + [ + ('/etc/ld.so.conf.d/dyninst-x86_64.conf', + '.......T. c /etc/ld.so.conf.d/dyninst-x86_64.conf', False), + ('/etc/ld.so.conf.d/dyninst-x86_64.conf', + 'S.5....T. c /etc/ld.so.conf.d/dyninst-x86_64.conf', True), + ('/etc/ld.so.conf.d/kernel-3.10.0-1160.el7.x86_64.conf', + '', False) + ]) +def test_is_modified(monkeypatch, config_path, run_result, is_modified): + def mocked_run(command, checked): + assert config_path in command + assert checked is False + exit_code = 1 if run_result else 0 + return {'stdout': run_result, 'exit_code': exit_code} + + monkeypatch.setattr(scandynamiclinkerconfiguration, 'run', mocked_run) + + _is_modified = scandynamiclinkerconfiguration._is_modified(config_path) + assert _is_modified == is_modified + + +@pytest.mark.parametrize(('config_path', + 'config_contents', 'run_result', + 'is_installed_rh_signed_package', 'is_modified', 'has_effective_lines'), + [ + ('/etc/ld.so.conf.d/dyninst-x86_64.conf', + ['/usr/lib64/dyninst\n'], 'dyninst', + True, False, True), # RH sighend package without modification - Not custom + ('/etc/ld.so.conf.d/dyninst-x86_64.conf', + ['/usr/lib64/my_dyninst\n'], 'dyninst', + True, True, True), # Was modified by user - Custom + ('/etc/custom/custom.conf', + ['/usr/lib64/custom'], 'custom', + False, None, True), # Third-party package - Custom + ('/etc/custom/custom.conf', + ['#/usr/lib64/custom\n'], 'custom', + False, None, False), # Third-party package without effective lines - Not custom + ('/etc/ld.so.conf.d/somelib.conf', + ['/usr/lib64/somelib\n'], CalledProcessError, + None, None, True), # User created configuration file - Custom + ('/etc/ld.so.conf.d/somelib.conf', + ['#/usr/lib64/somelib\n'], CalledProcessError, + None, None, False) # User created configuration file without effective lines - Not custom + ]) +def test_is_included_config_custom(monkeypatch, config_path, config_contents, run_result, + is_installed_rh_signed_package, is_modified, has_effective_lines): + 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 is_installed_rh_signed_package + + def mocked_read_file(path): + assert path == config_path + return config_contents + + monkeypatch.setattr(scandynamiclinkerconfiguration, 'run', mocked_run) + monkeypatch.setattr(scandynamiclinkerconfiguration, 'has_package', mocked_has_package) + monkeypatch.setattr(scandynamiclinkerconfiguration, '_read_file', mocked_read_file) + monkeypatch.setattr(scandynamiclinkerconfiguration, '_is_modified', lambda *_: is_modified) + monkeypatch.setattr(os.path, 'isfile', lambda _: True) + + result = scandynamiclinkerconfiguration._is_included_config_custom(config_path) + is_custom = not isinstance(run_result, str) or not is_installed_rh_signed_package or is_modified + is_custom &= has_effective_lines + assert result == is_custom diff --git a/repos/system_upgrade/common/models/dynamiclinker.py b/repos/system_upgrade/common/models/dynamiclinker.py new file mode 100644 index 0000000000..4dc107f4c6 --- /dev/null +++ b/repos/system_upgrade/common/models/dynamiclinker.py @@ -0,0 +1,41 @@ +from leapp.models import fields, Model +from leapp.topics import SystemFactsTopic + + +class LDConfigFile(Model): + """ + Represents a config file related to dynamic linker configuration + """ + topic = SystemFactsTopic + + path = fields.String() + """ Absolute path to the configuration file """ + + modified = fields.Boolean() + """ If True the file is considered custom and will trigger a report """ + + +class MainLDConfigFile(LDConfigFile): + """ + Represents the main configuration file of the dynamic linker /etc/ld.so.conf + """ + topic = SystemFactsTopic + + modified_lines = fields.List(fields.String(), default=[]) + """ Lines that are considered custom, generally those that are not includes of other configs """ + + +class DynamicLinkerConfiguration(Model): + """ + Facts about configuration of dynamic linker + """ + topic = SystemFactsTopic + + main_config = fields.Model(MainLDConfigFile) + """ The main configuration file of dynamic linker (/etc/ld.so.conf) """ + + included_configs = fields.List(fields.Model(LDConfigFile)) + """ All the configs that are included by the main configuration file """ + + used_variables = fields.List(fields.String(), default=[]) + """ Environment variables that are currently used to modify dynamic linker configuration """