Merge charm-hw-health:xavpaice/bug/1924863 into charm-hw-health:master
- Git
- lp:charm-hw-health
- xavpaice/bug/1924863
- Merge into master
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | James Troup | ||||
Approved revision: | 359b372eedf8b561ee71639bcc86a798c68d8570 | ||||
Merged at revision: | 00b40fba4d083ca0c7ae99afbdddef7eb27b05a9 | ||||
Proposed branch: | charm-hw-health:xavpaice/bug/1924863 | ||||
Merge into: | charm-hw-health:master | ||||
Diff against target: |
295 lines (+212/-1) 6 files modified
src/actions.yaml (+11/-0) src/actions/actions.py (+67/-0) src/actions/list-cron-actions (+1/-0) src/actions/run-cron-action (+1/-0) src/tests/unit/test_actions.py (+126/-1) src/tests/unit/test_run_good_job_file.cron (+6/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
🤖 prod-jenkaas-bootstack (community) | continuous-integration | Approve | |
James Troup (community) | Approve | ||
Diko Parvanov | Needs Fixing | ||
BootStack Reviewers | Pending | ||
Review via email: mp+408845@code.launchpad.net |
Commit message
Add two actions which will list the cron.d hwhealth tasks, and allow the juju user to run them.
Description of the change
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:2066f776d87
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:3588efaddbe
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:8adf0201c35
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:8adf0201c35
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
Xav Paice (xavpaice) wrote : | # |
7cda5c4918aad62
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:c825549842e
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:c50d75450c9
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:c50d75450c9
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Diko Parvanov (dparv) wrote : | # |
some comments below
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:9b2f45b1158
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:687094c9871
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
James Troup (elmo) wrote : | # |
See comments inline. I consider the following to be blocking: 1) '_*' glob, 2) either undocument 'all' as an argument to run-cron-action or implement it. The other things are more subjective/not blocking.
Adam Dyess (addyess) wrote : | # |
I incorporated @james' comments here to be more clear about how to invoke the actions.
I also ACTUALLY RAN the charm code within a real hw-health space to prove out the actions do as labeled on the box.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
A CI job is currently in progress. A follow up comment will be added when it completes.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 00b40fba4d083ca
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:359b372eedf
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/src/actions.yaml b/src/actions.yaml |
2 | index 888711d..ccd3be5 100644 |
3 | --- a/src/actions.yaml |
4 | +++ b/src/actions.yaml |
5 | @@ -26,3 +26,14 @@ ack-sel: |
6 | unack-sel: |
7 | description: | |
8 | Unset the date filter set by ack-sel action. |
9 | +"list-cron-actions": |
10 | + "description": | |
11 | + List all hw-health installed cron actions |
12 | +"run-cron-action": |
13 | + "description": | |
14 | + Run some or all of the the hw-health cron actions. |
15 | + "params": |
16 | + "entries": |
17 | + "type": "string" |
18 | + "description": "Optional comma separated list of cron actions to run." |
19 | + "required": [] |
20 | \ No newline at end of file |
21 | diff --git a/src/actions/actions.py b/src/actions/actions.py |
22 | index 4424545..62b2b50 100755 |
23 | --- a/src/actions/actions.py |
24 | +++ b/src/actions/actions.py |
25 | @@ -16,6 +16,9 @@ |
26 | """Define hw-health charm actions.""" |
27 | |
28 | import os |
29 | +import pathlib |
30 | +import re |
31 | +import shlex |
32 | import subprocess |
33 | import sys |
34 | from datetime import datetime |
35 | @@ -27,6 +30,9 @@ from hwhealth import tools |
36 | |
37 | |
38 | IPMI_SEL = "/usr/sbin/ipmi-sel" |
39 | +CRON_DIR = pathlib.Path("/etc") / "cron.d" |
40 | +RUN_PARTS_RE = re.compile("^[a-zA-Z0-9_-]+$") # run-parts(8) |
41 | +CRON_RE = re.compile(r"^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+)\s+(.*)") |
42 | |
43 | |
44 | def clear_sel(): |
45 | @@ -126,6 +132,65 @@ def unack_sel(): |
46 | action_fail("Action failed with {}".format(e)) |
47 | |
48 | |
49 | +def _get_cron_actions(): |
50 | + crond_files = CRON_DIR.glob("hwhealth_*") |
51 | + return { |
52 | + path.stem.split("_", 1)[-1] |
53 | + for path in crond_files |
54 | + if RUN_PARTS_RE.match(path.name) |
55 | + } |
56 | + |
57 | + |
58 | +def list_cron_actions(): |
59 | + actions = _get_cron_actions() |
60 | + message = "Cron Actions: {}".format(",".join(sorted(actions))) |
61 | + log("Action list-cron-actions completed\n" + message) |
62 | + action_set({"message": message}) |
63 | + |
64 | + |
65 | +def _run_job_file(fname): |
66 | + with fname.open() as f: |
67 | + job_lines = f.readlines() |
68 | + # match jobs according to crontab(5) man |
69 | + # blank, space or tab are all ignorable |
70 | + jobs = [m.groups() for m in (CRON_RE.match(line) for line in job_lines) if m] |
71 | + outs = [] |
72 | + for who, cmd in jobs: |
73 | + sudo_cmd = ["sudo", "-u", who] + shlex.split(cmd) |
74 | + try: |
75 | + out = subprocess.check_output(sudo_cmd, stderr=subprocess.STDOUT) |
76 | + except subprocess.CalledProcessError as e: |
77 | + action_fail("{}: {}\n\t{}".format(who, cmd, str(e))) |
78 | + out = e.output + e.stderr |
79 | + outs.append((sudo_cmd, out)) |
80 | + return outs |
81 | + |
82 | + |
83 | +def run_cron_action(): |
84 | + cron_entries = {entry for entry in action_get("entries").split(",") if entry} |
85 | + cron_actions = _get_cron_actions() |
86 | + to_run = cron_entries or cron_actions # if nothing specified, run all |
87 | + wont_run = to_run - cron_actions # any entries not capable of being run? |
88 | + to_run -= wont_run |
89 | + |
90 | + results = [ |
91 | + "{}\n\t{}".format(" ".join(cmd), result) |
92 | + for each in to_run |
93 | + if each in cron_actions |
94 | + for cmd, result in _run_job_file(CRON_DIR / "hwhealth_{}".format(each)) |
95 | + ] |
96 | + response = {} |
97 | + if results: |
98 | + response["message"] = "\n".join(results) |
99 | + |
100 | + if wont_run: |
101 | + # skipping the won't runs b/c they were not listed as a cron action |
102 | + response["skipped"] = "unavailable actions: {}".format( |
103 | + ",".join(sorted(wont_run)) |
104 | + ) |
105 | + action_set(response) |
106 | + |
107 | + |
108 | # A dictionary of all the defined actions to callables (which take |
109 | # parsed arguments). |
110 | ACTIONS = { |
111 | @@ -133,6 +198,8 @@ ACTIONS = { |
112 | "show-sel": show_sel, |
113 | "ack-sel": ack_sel, |
114 | "unack-sel": unack_sel, |
115 | + "list-cron-actions": list_cron_actions, |
116 | + "run-cron-action": run_cron_action, |
117 | } |
118 | |
119 | |
120 | diff --git a/src/actions/list-cron-actions b/src/actions/list-cron-actions |
121 | new file mode 120000 |
122 | index 0000000..405a394 |
123 | --- /dev/null |
124 | +++ b/src/actions/list-cron-actions |
125 | @@ -0,0 +1 @@ |
126 | +actions.py |
127 | \ No newline at end of file |
128 | diff --git a/src/actions/run-cron-action b/src/actions/run-cron-action |
129 | new file mode 120000 |
130 | index 0000000..405a394 |
131 | --- /dev/null |
132 | +++ b/src/actions/run-cron-action |
133 | @@ -0,0 +1 @@ |
134 | +actions.py |
135 | \ No newline at end of file |
136 | diff --git a/src/tests/unit/test_actions.py b/src/tests/unit/test_actions.py |
137 | index 3a3f4f2..15ee573 100644 |
138 | --- a/src/tests/unit/test_actions.py |
139 | +++ b/src/tests/unit/test_actions.py |
140 | @@ -12,13 +12,21 @@ |
141 | # See the License for the specific language governing permissions and |
142 | # limitations under the License. |
143 | |
144 | +import pathlib |
145 | import subprocess |
146 | import sys |
147 | +import tempfile |
148 | import unittest |
149 | import unittest.mock as mock |
150 | |
151 | sys.path.append(".") |
152 | -from actions.actions import clear_sel, show_sel # noqa:E402 |
153 | +from actions.actions import ( # noqa:E402 |
154 | + clear_sel, |
155 | + list_cron_actions, |
156 | + _run_job_file, |
157 | + run_cron_action, |
158 | + show_sel, |
159 | +) |
160 | |
161 | |
162 | class ClearSelTestCase(unittest.TestCase): |
163 | @@ -170,3 +178,120 @@ class ShowSelTestCase(unittest.TestCase): |
164 | "Action failed with Command '['bogus-cmd']' " |
165 | "returned non-zero exit status 1.", |
166 | ) |
167 | + |
168 | + |
169 | +class ListCronTestCase(unittest.TestCase): |
170 | + @mock.patch("actions.actions.action_set") |
171 | + @mock.patch("actions.actions.log") |
172 | + @mock.patch("actions.actions.CRON_DIR") |
173 | + def test_list_cron_jobs(self, mock_cron_dir, mock_log, mock_action_set): |
174 | + mock_cron_dir.glob.return_value = [ |
175 | + pathlib.Path("/path/to/hwhealth_ipmi"), |
176 | + pathlib.Path("/path/to/hwhealth_test"), |
177 | + ] |
178 | + |
179 | + list_cron_actions() |
180 | + mock_cron_dir.glob.assert_called_once_with("hwhealth_*") |
181 | + ui_response = "Cron Actions: ipmi,test" |
182 | + mock_log.assert_called_once_with( |
183 | + "Action list-cron-actions completed\n{}".format(ui_response) |
184 | + ) |
185 | + mock_action_set.assert_called_once_with({"message": ui_response}) |
186 | + |
187 | + @mock.patch("subprocess.check_output") |
188 | + def test_run_good_job_file(self, mock_check_output): |
189 | + test_file = pathlib.Path(__file__).parent / "test_run_good_job_file.cron" |
190 | + mock_check_output.side_effect = [ |
191 | + "result #1", |
192 | + "result #2", |
193 | + ] |
194 | + job_output = _run_job_file(test_file) |
195 | + assert job_output == [ |
196 | + ( |
197 | + [ |
198 | + "sudo", |
199 | + "-u", |
200 | + "nagios", |
201 | + "/run/my/command", |
202 | + "with", |
203 | + "some", |
204 | + "--arguments", |
205 | + ], |
206 | + "result #1", |
207 | + ), |
208 | + ( |
209 | + [ |
210 | + "sudo", |
211 | + "-u", |
212 | + "root", |
213 | + "/run/my/command", |
214 | + "with", |
215 | + "some", |
216 | + "--other-arguments", |
217 | + ], |
218 | + "result #2", |
219 | + ), |
220 | + ] |
221 | + |
222 | + @mock.patch("actions.actions.action_fail") |
223 | + @mock.patch("subprocess.check_output") |
224 | + def test_run_job_file_failure(self, mock_check_output, mock_action_fail): |
225 | + with tempfile.TemporaryDirectory() as tempdirname: |
226 | + test_file = pathlib.Path(tempdirname) / "test" |
227 | + with open(test_file, "w") as fp: |
228 | + fp.write("* * * * * root /run/my/command with some --other-arguments") |
229 | + |
230 | + mock_check_output.side_effect = [ |
231 | + subprocess.CalledProcessError( |
232 | + cmd="sudo", |
233 | + returncode=-1, |
234 | + output="No ", |
235 | + stderr="Way", |
236 | + ), |
237 | + ] |
238 | + job_output = _run_job_file(test_file) |
239 | + assert job_output == [ |
240 | + ( |
241 | + [ |
242 | + "sudo", |
243 | + "-u", |
244 | + "root", |
245 | + "/run/my/command", |
246 | + "with", |
247 | + "some", |
248 | + "--other-arguments", |
249 | + ], |
250 | + "No Way", |
251 | + ) |
252 | + ] |
253 | + mock_action_fail.assert_called_with( |
254 | + "root: /run/my/command with some --other-arguments\n\t" |
255 | + "Command 'sudo' died with <Signals.SIGHUP: 1>." |
256 | + ) |
257 | + |
258 | + @mock.patch("actions.actions._run_job_file") |
259 | + @mock.patch("actions.actions._get_cron_actions") |
260 | + @mock.patch("actions.actions.action_get") |
261 | + @mock.patch("actions.actions.action_set") |
262 | + def test_run_cron_action( |
263 | + self, |
264 | + mock_action_set, |
265 | + mock_action_get, |
266 | + mock_get_cron_actions, |
267 | + mock_run_job_file, |
268 | + ): |
269 | + mock_action_get.return_value = "test,garbage" |
270 | + mock_get_cron_actions.return_value = {"test"} |
271 | + mock_run_job_file.return_value = [ |
272 | + (["sudo", "-u", "root", "/run/my/cmd"], "works!") |
273 | + ] |
274 | + run_cron_action() |
275 | + mock_action_set.assert_called_once_with( |
276 | + { |
277 | + "message": "sudo -u root /run/my/cmd\n\tworks!", |
278 | + "skipped": "unavailable actions: garbage", |
279 | + } |
280 | + ) |
281 | + mock_run_job_file.assert_called_once_with( |
282 | + pathlib.Path("/etc/cron.d/hwhealth_test") |
283 | + ) |
284 | diff --git a/src/tests/unit/test_run_good_job_file.cron b/src/tests/unit/test_run_good_job_file.cron |
285 | new file mode 100644 |
286 | index 0000000..872e4a6 |
287 | --- /dev/null |
288 | +++ b/src/tests/unit/test_run_good_job_file.cron |
289 | @@ -0,0 +1,6 @@ |
290 | +# Cron Comments |
291 | + lines starting with spaces are comments |
292 | + so are lines starting with tab |
293 | +SHELL = /bin/sh |
294 | +* * * * * nagios /run/my/command with some --arguments |
295 | +* * * * * root /run/my/command with some --other-arguments |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.