Skip to content

Commit 4ae8996

Browse files
committed
add inhibitor for custom libraries registered by ld.so.conf
The in-place upgrade does not support custom libraries linked using the ld.so configuration. The new actor introduced in this commit detects if the configuration was tempered with and inhibits the upgrade in such case.
1 parent d6498b8 commit 4ae8996

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from leapp.actors import Actor
2+
from leapp.libraries.actor.checkldsoconfiguration import check_ld_so_configuration
3+
from leapp.models import InstalledRedHatSignedRPM, Report
4+
from leapp.tags import ChecksPhaseTag, IPUWorkflowTag
5+
6+
7+
class CheckLdSoConfiguration(Actor):
8+
"""
9+
Check for customization of ld.so configuration
10+
11+
The ld.so configuration files are used to overwrite standard library links
12+
in order to use different custom libraries. The in-place upgrade does not
13+
support customization of this configuration by user. This actor inhibits the
14+
upgrade upon detecting such customization.
15+
"""
16+
17+
name = 'check_ld_so_configuration'
18+
consumes = (InstalledRedHatSignedRPM,)
19+
produces = (Report,)
20+
tags = (ChecksPhaseTag, IPUWorkflowTag)
21+
22+
def process(self):
23+
check_ld_so_configuration()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import glob
2+
import os
3+
4+
from leapp import reporting
5+
from leapp.libraries.common.rpms import has_package
6+
from leapp.libraries.stdlib import api, CalledProcessError, run
7+
from leapp.models import InstalledRedHatSignedRPM
8+
9+
LD_SO_CONF_DIR = '/etc/ld.so.conf.d'
10+
LD_SO_CONF_MAIN = '/etc/ld.so.conf'
11+
LD_SO_CONF_DEFAULT_INCLUDE = 'ld.so.conf.d/*.conf'
12+
LIST_FORMAT_PREFIX = '\n - '
13+
14+
15+
def _read_file(file_path):
16+
with open(file_path, 'r') as fd:
17+
return fd.readlines()
18+
19+
20+
def _report_custom_ld_so_configuration(summary):
21+
reporting.create_report([
22+
reporting.Title(
23+
'Third party libraries linked with ld.so.conf are not supported for the in-place upgrade.'
24+
),
25+
reporting.Summary(summary),
26+
reporting.Remediation('Remove the custom ld.so configuration.'),
27+
reporting.RelatedResource('file', '/etc/ld.so.conf'),
28+
reporting.RelatedResource('directory', '/etc/ld.so.conf.d'),
29+
reporting.Severity(reporting.Severity.HIGH),
30+
reporting.Groups([reporting.Groups.OS_FACTS, reporting.Groups.INHIBITOR]),
31+
])
32+
33+
34+
def _is_included_ld_so_config_custom(config_path):
35+
if not os.path.isfile(config_path):
36+
return False
37+
38+
is_custom = False
39+
try:
40+
package_name = run(['rpm', '-qf', '--queryformat', '%{NAME}', config_path])['stdout']
41+
is_custom = not has_package(InstalledRedHatSignedRPM, package_name)
42+
except CalledProcessError:
43+
is_custom = True
44+
api.current_logger().debug('The following config file is{}considered a custom '
45+
'ld.so configuration: {}'.format(' ' if is_custom else ' NOT ', config_path))
46+
return is_custom
47+
48+
49+
def _parse_main_ld_so_config():
50+
config = _read_file(LD_SO_CONF_MAIN)
51+
52+
included_configs = []
53+
other_lines = []
54+
for line in config:
55+
line = line.strip()
56+
if line.startswith('include'):
57+
cfg_glob = line.split(' ', 1)[1].strip()
58+
cfg_glob = os.path.join('/etc', cfg_glob) if not os.path.isabs(cfg_glob) else cfg_glob
59+
included_configs.append(cfg_glob)
60+
elif line:
61+
other_lines.append(line)
62+
63+
return included_configs, other_lines
64+
65+
66+
def check_ld_so_configuration():
67+
included_configs, other_lines = _parse_main_ld_so_config()
68+
summary = ''
69+
70+
if other_lines:
71+
# The main ld.so config file is expected to only include additional configs that are owned by a package
72+
summary += ('The /etc/ld.so.conf file has unexpected contents:'
73+
'{}{}'.format(LIST_FORMAT_PREFIX,
74+
LIST_FORMAT_PREFIX.join(other_lines)))
75+
76+
is_default_include_present = '/etc/' + LD_SO_CONF_DEFAULT_INCLUDE in included_configs
77+
if not is_default_include_present:
78+
api.current_logger().debug('The default include "' + LD_SO_CONF_DEFAULT_INCLUDE +
79+
'" is not present in the ' + LD_SO_CONF_MAIN + ' file.')
80+
81+
if is_default_include_present and len(included_configs) != 1:
82+
# The additional included configs will most likely be created manually by the user
83+
# and therefore will get flagged as custom in the next part of this function
84+
api.current_logger().debug('The default include "' + LD_SO_CONF_DEFAULT_INCLUDE +
85+
'" is not the only include in the ' + LD_SO_CONF_MAIN + ' file.')
86+
87+
# Detect custom ld configs from the includes in the main ld.so.conf
88+
custom_ld_configs = []
89+
for cfg_glob in included_configs:
90+
custom_ld_configs += [cfg for cfg in glob.glob(cfg_glob) if _is_included_ld_so_config_custom(cfg)]
91+
92+
if custom_ld_configs:
93+
summary += ('\nThe following config files were marked as unsupported:'
94+
'{}{}'.format(LIST_FORMAT_PREFIX,
95+
LIST_FORMAT_PREFIX.join(custom_ld_configs)))
96+
97+
if summary:
98+
_report_custom_ld_so_configuration(summary)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import glob
2+
import os
3+
4+
import pytest
5+
6+
from leapp import reporting
7+
from leapp.libraries.actor import checkldsoconfiguration
8+
from leapp.libraries.common.testutils import create_report_mocked
9+
from leapp.libraries.stdlib import CalledProcessError
10+
from leapp.models import InstalledRedHatSignedRPM
11+
12+
INCLUDED_CONFIGS_GLOB_DICT_1 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
13+
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
14+
'/etc/ld.so.conf.d/bind-export-x86_64.conf']}
15+
16+
INCLUDED_CONFIGS_GLOB_DICT_2 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
17+
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
18+
'/etc/ld.so.conf.d/bind-export-x86_64.conf',
19+
'/etc/ld.so.conf.d/custom1.conf',
20+
'/etc/ld.so.conf.d/custom2.conf']}
21+
22+
INCLUDED_CONFIGS_GLOB_DICT_3 = {'/etc/ld.so.conf.d/*.conf': ['/etc/ld.so.conf.d/dyninst-x86_64.conf',
23+
'/etc/ld.so.conf.d/custom1.conf',
24+
'/etc/ld.so.conf.d/mariadb-x86_64.conf',
25+
'/etc/ld.so.conf.d/bind-export-x86_64.conf',
26+
'/etc/ld.so.conf.d/custom2.conf'],
27+
'/custom/path/*.conf': ['/custom/path/custom1.conf',
28+
'/custom/path/custom2.conf']}
29+
30+
31+
@pytest.mark.parametrize(('included_configs_glob_dict', 'other_lines', 'custom_configs'),
32+
[
33+
(INCLUDED_CONFIGS_GLOB_DICT_1, [], []),
34+
(INCLUDED_CONFIGS_GLOB_DICT_1, ['/custom/path.lib'], []),
35+
(INCLUDED_CONFIGS_GLOB_DICT_2, [], ['/etc/ld.so.conf.d/custom1.conf',
36+
'/etc/ld.so.conf.d/custom2.conf']),
37+
(INCLUDED_CONFIGS_GLOB_DICT_3, ['/custom/path.lib'], ['/etc/ld.so.conf.d/custom1.conf',
38+
'/etc/ld.so.conf.d/custom2.conf'
39+
'/custom/path/custom1.conf',
40+
'/custom/path/custom2.conf']),
41+
])
42+
def test_check_ld_so_configuration(monkeypatch, included_configs_glob_dict, other_lines, custom_configs):
43+
monkeypatch.setattr(reporting, 'create_report', create_report_mocked())
44+
monkeypatch.setattr(glob, 'glob', lambda glob: included_configs_glob_dict[glob])
45+
monkeypatch.setattr(checkldsoconfiguration, '_is_included_ld_so_config_custom',
46+
lambda config: config in custom_configs)
47+
monkeypatch.setattr(checkldsoconfiguration, '_parse_main_ld_so_config',
48+
lambda: (included_configs_glob_dict.keys(), other_lines))
49+
50+
checkldsoconfiguration.check_ld_so_configuration()
51+
52+
report_expected = custom_configs or other_lines
53+
if not report_expected:
54+
assert reporting.create_report.called == 0
55+
return
56+
57+
assert reporting.create_report.called == 1
58+
assert 'ld.so.conf' in reporting.create_report.reports[0]['title']
59+
summary = reporting.create_report.reports[0]['summary']
60+
61+
all_configs = []
62+
for configs in included_configs_glob_dict.values():
63+
all_configs += configs
64+
65+
if custom_configs:
66+
assert 'The following config files were marked as unsupported:' in summary
67+
68+
for config in all_configs:
69+
assert (config in custom_configs) == (config in summary)
70+
71+
if other_lines:
72+
assert 'The /etc/ld.so.conf file has unexpected contents' in summary
73+
74+
for other_line in other_lines:
75+
assert other_line in summary
76+
77+
78+
@pytest.mark.parametrize(('config_contents', 'included_config_paths', 'other_lines'),
79+
[
80+
(['include ld.so.conf.d/*.conf\n'],
81+
['/etc/ld.so.conf.d/*.conf'], []),
82+
(['include ld.so.conf.d/*.conf\n', '\n', '/custom/path.lib\n'],
83+
['/etc/ld.so.conf.d/*.conf'], ['/custom/path.lib']),
84+
(['include ld.so.conf.d/*.conf\n', 'include /custom/path.conf\n'],
85+
['/etc/ld.so.conf.d/*.conf', '/custom/path.conf'], []),
86+
([' \n'],
87+
[], [])
88+
])
89+
def test_parse_main_ld_so_config(monkeypatch, config_contents, included_config_paths, other_lines):
90+
def mocked_read_file(path):
91+
assert path == checkldsoconfiguration.LD_SO_CONF_MAIN
92+
return config_contents
93+
94+
monkeypatch.setattr(checkldsoconfiguration, '_read_file', mocked_read_file)
95+
96+
_included_config_paths, _other_lines = checkldsoconfiguration._parse_main_ld_so_config()
97+
98+
assert _included_config_paths == included_config_paths
99+
assert _other_lines == other_lines
100+
101+
102+
@pytest.mark.parametrize(('config_path', 'run_result', 'package_exists', 'is_custom'),
103+
[
104+
('/etc/ld.so.conf.d/dyninst-x86_64.conf', 'dyninst', True, False),
105+
('/etc/ld.so.conf.d/somelib.conf', CalledProcessError, False, True),
106+
('/etc/custom/custom.conf', 'custom', False, True)
107+
])
108+
def test_is_included_ld_so_config_custom(monkeypatch, config_path, run_result, package_exists, is_custom):
109+
def mocked_run(command):
110+
assert config_path in command
111+
if run_result and not isinstance(run_result, str):
112+
raise CalledProcessError("message", ["command"], "result")
113+
return {'stdout': run_result}
114+
115+
def mocked_has_package(model, package_name):
116+
assert model is InstalledRedHatSignedRPM
117+
assert package_name == run_result
118+
return package_exists
119+
120+
monkeypatch.setattr(checkldsoconfiguration, 'run', mocked_run)
121+
monkeypatch.setattr(checkldsoconfiguration, 'has_package', mocked_has_package)
122+
monkeypatch.setattr(os.path, 'isfile', lambda _: True)
123+
124+
result = checkldsoconfiguration._is_included_ld_so_config_custom(config_path)
125+
assert result == is_custom

0 commit comments

Comments
 (0)