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
diff --git a/src/files/ilorest/cron_ilorest.py b/src/files/ilorest/cron_ilorest.py
index 737ae7a..08eb1c7 100755
--- a/src/files/ilorest/cron_ilorest.py
+++ b/src/files/ilorest/cron_ilorest.py
@@ -46,6 +46,7 @@ class CronILOrest:
46 """Parse CLI args during instance creation."""46 """Parse CLI args during instance creation."""
47 self.args = self.parse_args(argv)47 self.args = self.parse_args(argv)
48 self._ilorest_version = None48 self._ilorest_version = None
49 self._ilorest_binary = "/usr/sbin/ilorest"
4950
50 @property51 @property
51 def ilorest_version(self):52 def ilorest_version(self):
@@ -60,13 +61,15 @@ class CronILOrest:
60 if self._ilorest_version is not None:61 if self._ilorest_version is not None:
61 return self._ilorest_version62 return self._ilorest_version
6263
63 version_cmd = ["/usr/sbin/ilorest", "--version"]64 ilorest_args = ["--version"]
64 try:65 try:
65 command_output = check_output(version_cmd, universal_newlines=True)66 command_output = self._run_ilorest(*ilorest_args)
66 version_string = command_output.rsplit(maxsplit=1)67 version_string = command_output.rsplit(maxsplit=1)
67 if not version_string:68 if not version_string:
68 print(69 print(
69 "Command '%s' did not return any value." % version_cmd,70 "Command '{}' did not return any value.".format(
71 [self._ilorest_binary, *ilorest_args]
72 ),
70 file=sys.stderr,73 file=sys.stderr,
71 )74 )
72 self._ilorest_version = None75 self._ilorest_version = None
@@ -136,17 +139,32 @@ class CronILOrest:
136139
137 return self._walk_selector(jsondata, [selector])140 return self._walk_selector(jsondata, [selector])
138141
142 def _run_ilorest(self, *args):
143 """Run ilorest executable and return stdout string.
144
145 The ilorest ELF includes several shared objects that are unpacked
146 to the tempdir during runtime. This causes issues in cases where
147 the tempdir(usually /tmp) is mounted with the mount option 'noexec'.
148 This wrapper function is essentially in place to accomodate this
149 by providing the binary with an alternative tempdir that it can use.
150
151 See: Launchpad #1935032, #1979587
152 """
153 return check_output(
154 [self._ilorest_binary, *args],
155 env={**os.environ, "TMP": "/var/lib/hw-health"},
156 universal_newlines=True,
157 )
158
139 def _get_json_ilorest_output(self, selector, version):159 def _get_json_ilorest_output(self, selector, version):
140 cmd = ["/usr/sbin/ilorest", "list", "-j", "--selector={}".format(selector)]160 ilorest_args = ["list", "-j", "--selector={}".format(selector)]
141 if int(version[0]) >= 3:161 if int(version[0]) >= 3:
142 # ilorest above version 3.0 (included) has '--refresh' flag and162 # ilorest above version 3.0 (included) has '--refresh' flag and
143 # should use it to force fetching new data instead of using cached163 # should use it to force fetching new data instead of using cached
144 # values164 # values
145 cmd.append("--refresh")165 ilorest_args.append("--refresh")
146166
147 return check_output( # LP#1935032 fix for noexec /tmp167 return self._run_ilorest(*ilorest_args)
148 cmd, env={**os.environ, "TMP": "/var/lib/hw-health"}
149 ).decode("UTF-8")
150168
151 def _get_health_status_message(self, json_item, crumb_trail=[]):169 def _get_health_status_message(self, json_item, crumb_trail=[]):
152 desc = json_item["Name"]170 desc = json_item["Name"]
diff --git a/src/tests/unit/test_cron_ilorest.py b/src/tests/unit/test_cron_ilorest.py
index 796c76e..d9f29bf 100644
--- a/src/tests/unit/test_cron_ilorest.py
+++ b/src/tests/unit/test_cron_ilorest.py
@@ -50,56 +50,59 @@ class TestCronILOrest(unittest.TestCase):
50 errors = cronilorest.check_selector(selector)50 errors = cronilorest.check_selector(selector)
51 assert errors != []51 assert errors != []
5252
53 @mock.patch.object(cron_ilorest, "check_output")53 @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
54 def test_version_getter_success(self, check_output_mock):54 def test_version_getter_success(self, run_ilorest_mock):
55 """Test getting and parsing iloret version string."""55 """Test getting and parsing iloret version string."""
56 version_string = "RESTful Interface Tool 3.5.0.0"56 version_string = "RESTful Interface Tool 3.5.0.0"
57 expected_version = "3.5.0.0"57 expected_version = "3.5.0.0"
58 check_output_mock.return_value = version_string58 run_ilorest_mock.return_value = version_string
5959
60 cronilorest = cron_ilorest.CronILOrest()60 cronilorest = cron_ilorest.CronILOrest()
6161
62 self.assertEqual(cronilorest.ilorest_version, expected_version)62 self.assertEqual(cronilorest.ilorest_version, expected_version)
63 run_ilorest_mock.assert_called_once_with("--version")
6364
64 @mock.patch("builtins.print")65 @mock.patch("builtins.print")
65 @mock.patch.object(cron_ilorest, "check_output")66 @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
66 def test_version_getter_no_value(self, check_output_mock, print_mock):67 def test_version_getter_no_value(self, run_ilorest_mock, print_mock):
67 """Test handling when ilorest version command does not return value."""68 """Test handling when ilorest version command does not return value."""
68 version_string = ""69 version_string = ""
69 expected_version = None70 expected_version = None
70 expected_msg = (71 expected_msg = (
71 "Command '['/usr/sbin/ilorest', '--version']' did not" " return any value."72 "Command '['/usr/sbin/ilorest', '--version']' did not" " return any value."
72 )73 )
73 check_output_mock.return_value = version_string74 run_ilorest_mock.return_value = version_string
7475
75 cronilorest = cron_ilorest.CronILOrest()76 cronilorest = cron_ilorest.CronILOrest()
7677
77 self.assertEqual(cronilorest.ilorest_version, expected_version)78 self.assertEqual(cronilorest.ilorest_version, expected_version)
78 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)79 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)
80 run_ilorest_mock.assert_called_once_with("--version")
7981
80 @mock.patch("builtins.print")82 @mock.patch("builtins.print")
81 @mock.patch.object(cron_ilorest, "check_output")83 @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
82 def test_version_getter_invalid_value(self, check_output_mock, print_mock):84 def test_version_getter_invalid_value(self, run_ilorest_mock, print_mock):
83 """Test handling when ilorest version command returns unexpected value."""85 """Test handling when ilorest version command returns unexpected value."""
84 bad_version = "3.5.0.a"86 bad_version = "3.5.0.a"
85 version_string = "RESTful Interface Tool " + bad_version87 version_string = "RESTful Interface Tool " + bad_version
86 expected_version = None88 expected_version = None
87 expected_msg = "Failed to parse ilorest version: '%s'" % bad_version89 expected_msg = "Failed to parse ilorest version: '%s'" % bad_version
88 check_output_mock.return_value = version_string90 run_ilorest_mock.return_value = version_string
8991
90 cronilorest = cron_ilorest.CronILOrest()92 cronilorest = cron_ilorest.CronILOrest()
9193
92 self.assertEqual(cronilorest.ilorest_version, expected_version)94 self.assertEqual(cronilorest.ilorest_version, expected_version)
93 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)95 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)
96 run_ilorest_mock.assert_called_once_with("--version")
9497
95 @mock.patch("builtins.print")98 @mock.patch("builtins.print")
96 @mock.patch.object(cron_ilorest, "check_output")99 @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
97 def test_version_getter_cmd_fail(self, check_output_mock, print_mock):100 def test_version_getter_cmd_fail(self, run_ilorest_mock, print_mock):
98 """Test handling when ilorest version command returns non-zero code."""101 """Test handling when ilorest version command returns non-zero code."""
99 error_msg = "Ilorest command failed"102 error_msg = "Ilorest command failed"
100 expected_version = None103 expected_version = None
101 expected_msg = "Failed to get ilorest version: %s" % error_msg104 expected_msg = "Failed to get ilorest version: %s" % error_msg
102 check_output_mock.side_effect = subprocess.CalledProcessError(105 run_ilorest_mock.side_effect = subprocess.CalledProcessError(
103 output=error_msg, cmd="/usr/sbin/ilorest --version", returncode=1106 output=error_msg, cmd="/usr/sbin/ilorest --version", returncode=1
104 )107 )
105108
@@ -107,9 +110,10 @@ class TestCronILOrest(unittest.TestCase):
107110
108 self.assertEqual(cronilorest.ilorest_version, expected_version)111 self.assertEqual(cronilorest.ilorest_version, expected_version)
109 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)112 print_mock.assert_called_once_with(expected_msg, file=sys.stderr)
113 run_ilorest_mock.assert_called_once_with("--version")
110114
111 @mock.patch.object(cron_ilorest, "check_output")115 @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
112 def test_get_json_ilorest_output_v3(self, check_output_mock):116 def test_get_json_ilorest_output_v3(self, run_ilorest_mock):
113 """Test that _get_json_ilorest_output runs correct command for version 3.117 """Test that _get_json_ilorest_output runs correct command for version 3.
114118
115 ilorest tool got new argument `--refresh` in version 3.0.0 that forces119 ilorest tool got new argument `--refresh` in version 3.0.0 that forces
@@ -117,27 +121,24 @@ class TestCronILOrest(unittest.TestCase):
117 """121 """
118 selector = "Chassis"122 selector = "Chassis"
119 version = "3.0.0"123 version = "3.0.0"
120 expected_cmd = [124 expected_args = [
121 "/usr/sbin/ilorest",
122 "list",125 "list",
123 "-j",126 "-j",
124 "--selector=%s" % selector,127 "--selector=%s" % selector,
125 "--refresh",128 "--refresh",
126 ]129 ]
127 expected_output = b"Chassis status"130 expected_output = "Chassis status"
128131
129 check_output_mock.return_value = expected_output132 run_ilorest_mock.return_value = expected_output
130133
131 cronilorest = cron_ilorest.CronILOrest()134 cronilorest = cron_ilorest.CronILOrest()
132 result = cronilorest._get_json_ilorest_output(selector, version)135 result = cronilorest._get_json_ilorest_output(selector, version)
133136
134 self.assertEqual(result, expected_output.decode("UTF-8"))137 self.assertEqual(result, expected_output)
135 check_output_mock.assert_called_once_with(138 run_ilorest_mock.assert_called_once_with(*expected_args)
136 expected_cmd, env={**os.environ, "TMP": "/var/lib/hw-health"}
137 )
138139
139 @mock.patch.object(cron_ilorest, "check_output")140 @mock.patch.object(cron_ilorest.CronILOrest, "_run_ilorest")
140 def test_get_json_ilorest_output_v2(self, check_output_mock):141 def test_get_json_ilorest_output_v2(self, run_ilorest_mock):
141 """Test that _get_json_ilorest_output runs correct command for version 2.142 """Test that _get_json_ilorest_output runs correct command for version 2.
142143
143 ilorest tool does not have an argument `--refresh` in versions below 3.0.0144 ilorest tool does not have an argument `--refresh` in versions below 3.0.0
@@ -145,15 +146,24 @@ class TestCronILOrest(unittest.TestCase):
145 """146 """
146 selector = "Chassis"147 selector = "Chassis"
147 version = "2.5.1"148 version = "2.5.1"
148 expected_cmd = ["/usr/sbin/ilorest", "list", "-j", "--selector=%s" % selector]149 expected_args = ["list", "-j", "--selector=%s" % selector]
149 expected_output = b"Chassis status"150 expected_output = "Chassis status"
150151
151 check_output_mock.return_value = expected_output152 run_ilorest_mock.return_value = expected_output
152153
153 cronilorest = cron_ilorest.CronILOrest()154 cronilorest = cron_ilorest.CronILOrest()
154 result = cronilorest._get_json_ilorest_output(selector, version)155 result = cronilorest._get_json_ilorest_output(selector, version)
155156
156 self.assertEqual(result, expected_output.decode("UTF-8"))157 self.assertEqual(result, expected_output)
158 run_ilorest_mock.assert_called_once_with(*expected_args)
159
160 @mock.patch.object(cron_ilorest, "check_output")
161 def test_run_ilorest(self, check_output_mock):
162 args = ["foo", "bar"]
163 cron_ilorest.CronILOrest()._run_ilorest(*args)
164
157 check_output_mock.assert_called_once_with(165 check_output_mock.assert_called_once_with(
158 expected_cmd, env={**os.environ, "TMP": "/var/lib/hw-health"}166 ["/usr/sbin/ilorest", *args],
167 env={**os.environ, "TMP": "/var/lib/hw-health"},
168 universal_newlines=True,
159 )169 )
diff --git a/src/tox.ini b/src/tox.ini
index 71e969c..afa9c71 100644
--- a/src/tox.ini
+++ b/src/tox.ini
@@ -26,7 +26,7 @@ passenv =
26[testenv:lint]26[testenv:lint]
27commands =27commands =
28 flake828 flake8
29 black --check --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" .29 black --diff --color --check --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" .
30deps =30deps =
31 black31 black
32 flake832 flake8
@@ -73,7 +73,8 @@ commands =
73 --cov=files \73 --cov=files \
74 --cov-report=term \74 --cov-report=term \
75 --cov-report=annotate:report/annotated \75 --cov-report=annotate:report/annotated \
76 --cov-report=html:report/html76 --cov-report=html:report/html \
77 --cov-report=term-missing
77deps = -r{toxinidir}/tests/unit/requirements.txt78deps = -r{toxinidir}/tests/unit/requirements.txt
7879
79[testenv:func]80[testenv:func]

Subscribers

People subscribed via source and target branches

to all changes: