Merge charm-hw-health:xavpaice/bug/1924863 into charm-hw-health:master

Proposed by Adam Dyess
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)
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.

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 :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
Xav Paice (xavpaice) wrote :

7cda5c4918aad62c0811d210b779ae293df56bea got missed, the branch needed a rebase. I've done that, and re-pushed.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
Diko Parvanov (dparv) wrote :

some comments below

review: Needs Fixing
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :

A CI job is currently in progress. A follow up comment will be added when it completes.

Revision history for this message
James Troup (elmo) wrote :

LGTM, thanks!

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

Change successfully merged at revision 00b40fba4d083ca0c7ae99afbdddef7eb27b05a9

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/actions.yaml b/src/actions.yaml
2index 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
21diff --git a/src/actions/actions.py b/src/actions/actions.py
22index 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
120diff --git a/src/actions/list-cron-actions b/src/actions/list-cron-actions
121new file mode 120000
122index 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
128diff --git a/src/actions/run-cron-action b/src/actions/run-cron-action
129new file mode 120000
130index 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
136diff --git a/src/tests/unit/test_actions.py b/src/tests/unit/test_actions.py
137index 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+ )
284diff --git a/src/tests/unit/test_run_good_job_file.cron b/src/tests/unit/test_run_good_job_file.cron
285new file mode 100644
286index 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

Subscribers

No one subscribed via source and target branches