Merge ~mertkirpici/charm-hw-health:lp/1979587 into charm-hw-health:master

Proposed by Mert Kirpici
Status: Merged
Approved by: Martin Kalcok
Approved revision: 16f70181a08caab9e015cac327963a005713bfb1
Merged at revision: 27c38cd9129285de453b12b287fdf818d439d0d1
Proposed branch: ~mertkirpici/charm-hw-health:lp/1979587
Merge into: charm-hw-health:master
Diff against target: 246 lines (+68/-39)
3 files modified
src/files/ilorest/cron_ilorest.py (+26/-8)
src/tests/unit/test_cron_ilorest.py (+39/-29)
src/tox.ini (+3/-2)
Reviewer Review Type Date Requested Status
Martin Kalcok (community) Approve
🤖 prod-jenkaas-bootstack (community) continuous-integration Approve
Erhan Sunar (community) Approve
Eric Chen Approve
Robert Gildein Approve
Review via email: mp+431492@code.launchpad.net

Commit message

Close LP #1979587

Description of the change

The ilorest ELF includes several shared objects that are unpacked to the tempdir during runtime. This causes issues in cases where the tempdir(usually /tmp) is mounted with the mount option 'noexec'. e.g. CIS hardening.

The introduced _run_ilorest private wrapper function is introduced to accomodate this by providing the binary with an alternative tempdir that it can use.

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
Robert Gildein (rgildein) wrote :

LGTM

review: Approve
Revision history for this message
Martin Kalcok (martin-kalcok) wrote :

Overall LGTM, thanks. Just one question, is the hardcoded TMP dir guaranteed to exist?

Also one suggestion, rather than asserting that specific command was executed in each "test_version_getter_*" tests, I think I'd be better to assert that the wrapper (`_run_ilorest()`) was called and then have specific unit test for wrapper. It'll be easier in the future to update unit tests in case the wrapper changes internally

Revision history for this message
Eric Chen (eric-chen) :
review: Approve
Revision history for this message
Erhan Sunar (esunar) :
review: Approve
Revision history for this message
Mert Kirpici (mertkirpici) wrote :

Thanks for the reviews everyone!
I want to apply Martin's suggestion and push an update to the tests before merging, so I would appreciate if we wait for that update and then merge this.

> Overall LGTM, thanks. Just one question, is the hardcoded TMP dir guaranteed to exist?

Yes this is part of the previous implementation, we are taking care of this in the install handler.
https://git.launchpad.net/~mertkirpici/charm-hw-health/tree/src/reactive/hw_health.py#n39

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
Mert Kirpici (mertkirpici) wrote :

pushed the update that implements a new test for the introduced function and adapting the existing tests to match the source code. ci passed. we are ready to merge this.

Revision history for this message
Martin Kalcok (martin-kalcok) wrote :

Thanks, +1

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 27c38cd9129285de453b12b287fdf818d439d0d1

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/files/ilorest/cron_ilorest.py b/src/files/ilorest/cron_ilorest.py
2index 737ae7a..08eb1c7 100755
3--- a/src/files/ilorest/cron_ilorest.py
4+++ b/src/files/ilorest/cron_ilorest.py
5@@ -46,6 +46,7 @@ class CronILOrest:
6 """Parse CLI args during instance creation."""
7 self.args = self.parse_args(argv)
8 self._ilorest_version = None
9+ self._ilorest_binary = "/usr/sbin/ilorest"
10
11 @property
12 def ilorest_version(self):
13@@ -60,13 +61,15 @@ class CronILOrest:
14 if self._ilorest_version is not None:
15 return self._ilorest_version
16
17- version_cmd = ["/usr/sbin/ilorest", "--version"]
18+ ilorest_args = ["--version"]
19 try:
20- command_output = check_output(version_cmd, universal_newlines=True)
21+ command_output = self._run_ilorest(*ilorest_args)
22 version_string = command_output.rsplit(maxsplit=1)
23 if not version_string:
24 print(
25- "Command '%s' did not return any value." % version_cmd,
26+ "Command '{}' did not return any value.".format(
27+ [self._ilorest_binary, *ilorest_args]
28+ ),
29 file=sys.stderr,
30 )
31 self._ilorest_version = None
32@@ -136,17 +139,32 @@ class CronILOrest:
33
34 return self._walk_selector(jsondata, [selector])
35
36+ def _run_ilorest(self, *args):
37+ """Run ilorest executable and return stdout string.
38+
39+ The ilorest ELF includes several shared objects that are unpacked
40+ to the tempdir during runtime. This causes issues in cases where
41+ the tempdir(usually /tmp) is mounted with the mount option 'noexec'.
42+ This wrapper function is essentially in place to accomodate this
43+ by providing the binary with an alternative tempdir that it can use.
44+
45+ See: Launchpad #1935032, #1979587
46+ """
47+ return check_output(
48+ [self._ilorest_binary, *args],
49+ env={**os.environ, "TMP": "/var/lib/hw-health"},
50+ universal_newlines=True,
51+ )
52+
53 def _get_json_ilorest_output(self, selector, version):
54- cmd = ["/usr/sbin/ilorest", "list", "-j", "--selector={}".format(selector)]
55+ ilorest_args = ["list", "-j", "--selector={}".format(selector)]
56 if int(version[0]) >= 3:
57 # ilorest above version 3.0 (included) has '--refresh' flag and
58 # should use it to force fetching new data instead of using cached
59 # values
60- cmd.append("--refresh")
61+ ilorest_args.append("--refresh")
62
63- return check_output( # LP#1935032 fix for noexec /tmp
64- cmd, env={**os.environ, "TMP": "/var/lib/hw-health"}
65- ).decode("UTF-8")
66+ return self._run_ilorest(*ilorest_args)
67
68 def _get_health_status_message(self, json_item, crumb_trail=[]):
69 desc = json_item["Name"]
70diff --git a/src/tests/unit/test_cron_ilorest.py b/src/tests/unit/test_cron_ilorest.py
71index 796c76e..d9f29bf 100644
72--- a/src/tests/unit/test_cron_ilorest.py
73+++ b/src/tests/unit/test_cron_ilorest.py
74@@ -50,56 +50,59 @@ class TestCronILOrest(unittest.TestCase):
75 errors = cronilorest.check_selector(selector)
76 assert errors != []
77
78- @mock.patch.object(cron_ilorest, "check_output")
79- def test_version_getter_success(self, check_output_mock):
80+ @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
81+ def test_version_getter_success(self, run_ilorest_mock):
82 """Test getting and parsing iloret version string."""
83 version_string = "RESTful Interface Tool 3.5.0.0"
84 expected_version = "3.5.0.0"
85- check_output_mock.return_value = version_string
86+ run_ilorest_mock.return_value = version_string
87
88 cronilorest = cron_ilorest.CronILOrest()
89
90 self.assertEqual(cronilorest.ilorest_version, expected_version)
91+ run_ilorest_mock.assert_called_once_with("--version")
92
93 @mock.patch("builtins.print")
94- @mock.patch.object(cron_ilorest, "check_output")
95- def test_version_getter_no_value(self, check_output_mock, print_mock):
96+ @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
97+ def test_version_getter_no_value(self, run_ilorest_mock, print_mock):
98 """Test handling when ilorest version command does not return value."""
99 version_string = ""
100 expected_version = None
101 expected_msg = (
102 "Command '['/usr/sbin/ilorest', '--version']' did not" " return any value."
103 )
104- check_output_mock.return_value = version_string
105+ run_ilorest_mock.return_value = version_string
106
107 cronilorest = cron_ilorest.CronILOrest()
108
109 self.assertEqual(cronilorest.ilorest_version, expected_version)
110 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)
111+ run_ilorest_mock.assert_called_once_with("--version")
112
113 @mock.patch("builtins.print")
114- @mock.patch.object(cron_ilorest, "check_output")
115- def test_version_getter_invalid_value(self, check_output_mock, print_mock):
116+ @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
117+ def test_version_getter_invalid_value(self, run_ilorest_mock, print_mock):
118 """Test handling when ilorest version command returns unexpected value."""
119 bad_version = "3.5.0.a"
120 version_string = "RESTful Interface Tool " + bad_version
121 expected_version = None
122 expected_msg = "Failed to parse ilorest version: '%s'" % bad_version
123- check_output_mock.return_value = version_string
124+ run_ilorest_mock.return_value = version_string
125
126 cronilorest = cron_ilorest.CronILOrest()
127
128 self.assertEqual(cronilorest.ilorest_version, expected_version)
129 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)
130+ run_ilorest_mock.assert_called_once_with("--version")
131
132 @mock.patch("builtins.print")
133- @mock.patch.object(cron_ilorest, "check_output")
134- def test_version_getter_cmd_fail(self, check_output_mock, print_mock):
135+ @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
136+ def test_version_getter_cmd_fail(self, run_ilorest_mock, print_mock):
137 """Test handling when ilorest version command returns non-zero code."""
138 error_msg = "Ilorest command failed"
139 expected_version = None
140 expected_msg = "Failed to get ilorest version: %s" % error_msg
141- check_output_mock.side_effect = subprocess.CalledProcessError(
142+ run_ilorest_mock.side_effect = subprocess.CalledProcessError(
143 output=error_msg, cmd="/usr/sbin/ilorest --version", returncode=1
144 )
145
146@@ -107,9 +110,10 @@ class TestCronILOrest(unittest.TestCase):
147
148 self.assertEqual(cronilorest.ilorest_version, expected_version)
149 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)
150+ run_ilorest_mock.assert_called_once_with("--version")
151
152- @mock.patch.object(cron_ilorest, "check_output")
153- def test_get_json_ilorest_output_v3(self, check_output_mock):
154+ @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
155+ def test_get_json_ilorest_output_v3(self, run_ilorest_mock):
156 """Test that _get_json_ilorest_output runs correct command for version 3.
157
158 ilorest tool got new argument `--refresh` in version 3.0.0 that forces
159@@ -117,27 +121,24 @@ class TestCronILOrest(unittest.TestCase):
160 """
161 selector = "Chassis"
162 version = "3.0.0"
163- expected_cmd = [
164- "/usr/sbin/ilorest",
165+ expected_args = [
166 "list",
167 "-j",
168 "--selector=%s" % selector,
169 "--refresh",
170 ]
171- expected_output = b"Chassis status"
172+ expected_output = "Chassis status"
173
174- check_output_mock.return_value = expected_output
175+ run_ilorest_mock.return_value = expected_output
176
177 cronilorest = cron_ilorest.CronILOrest()
178 result = cronilorest._get_json_ilorest_output(selector, version)
179
180- self.assertEqual(result, expected_output.decode("UTF-8"))
181- check_output_mock.assert_called_once_with(
182- expected_cmd, env={**os.environ, "TMP": "/var/lib/hw-health"}
183- )
184+ self.assertEqual(result, expected_output)
185+ run_ilorest_mock.assert_called_once_with(*expected_args)
186
187- @mock.patch.object(cron_ilorest, "check_output")
188- def test_get_json_ilorest_output_v2(self, check_output_mock):
189+ @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
190+ def test_get_json_ilorest_output_v2(self, run_ilorest_mock):
191 """Test that _get_json_ilorest_output runs correct command for version 2.
192
193 ilorest tool does not have an argument `--refresh` in versions below 3.0.0
194@@ -145,15 +146,24 @@ class TestCronILOrest(unittest.TestCase):
195 """
196 selector = "Chassis"
197 version = "2.5.1"
198- expected_cmd = ["/usr/sbin/ilorest", "list", "-j", "--selector=%s" % selector]
199- expected_output = b"Chassis status"
200+ expected_args = ["list", "-j", "--selector=%s" % selector]
201+ expected_output = "Chassis status"
202
203- check_output_mock.return_value = expected_output
204+ run_ilorest_mock.return_value = expected_output
205
206 cronilorest = cron_ilorest.CronILOrest()
207 result = cronilorest._get_json_ilorest_output(selector, version)
208
209- self.assertEqual(result, expected_output.decode("UTF-8"))
210+ self.assertEqual(result, expected_output)
211+ run_ilorest_mock.assert_called_once_with(*expected_args)
212+
213+ @mock.patch.object(cron_ilorest, "check_output")
214+ def test_run_ilorest(self, check_output_mock):
215+ args = ["foo", "bar"]
216+ cron_ilorest.CronILOrest()._run_ilorest(*args)
217+
218 check_output_mock.assert_called_once_with(
219- expected_cmd, env={**os.environ, "TMP": "/var/lib/hw-health"}
220+ ["/usr/sbin/ilorest", *args],
221+ env={**os.environ, "TMP": "/var/lib/hw-health"},
222+ universal_newlines=True,
223 )
224diff --git a/src/tox.ini b/src/tox.ini
225index 71e969c..afa9c71 100644
226--- a/src/tox.ini
227+++ b/src/tox.ini
228@@ -26,7 +26,7 @@ passenv =
229 [testenv:lint]
230 commands =
231 flake8
232- black --check --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" .
233+ black --diff --color --check --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" .
234 deps =
235 black
236 flake8
237@@ -73,7 +73,8 @@ commands =
238 --cov=files \
239 --cov-report=term \
240 --cov-report=annotate:report/annotated \
241- --cov-report=html:report/html
242+ --cov-report=html:report/html \
243+ --cov-report=term-missing
244 deps = -r{toxinidir}/tests/unit/requirements.txt
245
246 [testenv:func]

Subscribers

People subscribed via source and target branches

to all changes: