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

Proposed by Xav Paice
Status: Rejected
Rejected by: Haw Loeung
Proposed branch: ~xavpaice/charm-hw-health:bug/1924863
Merge into: charm-hw-health:master
Diff against target: 256 lines (+179/-1)
5 files modified
src/actions.yaml (+10/-0)
src/actions/actions.py (+48/-0)
src/actions/list-cron-actions (+1/-0)
src/actions/run-cron-action (+1/-0)
src/tests/unit/test_actions.py (+119/-1)
Reviewer Review Type Date Requested Status
James Troup (community) Needs Fixing
Tom Haddon Abstain
🤖 prod-jenkaas-bootstack (community) continuous-integration Needs Fixing
BootStack Reviewers mr tracking; do not claim Pending
BootStack Reviewers Pending
BootStack Reviewers Pending
Canonical BootStack Charmers Pending
Review via email: mp+404504@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
Xav Paice (xavpaice) wrote :

rebased and resubmitted; change looks good but CI was failing because it needed some unrelated fixes.

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
🤖 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
Tom Haddon (mthaddon) wrote :

Abstaining, just claimed the review to take it off the canonical-is-reviewers dashboard, mergebot has been switched to add bootstack-reviewers for future MPs.

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

See comments inline

review: Needs Fixing

Unmerged commits

2c550ee... by Adam Dyess

Fix linting issues around testing cron.d updated

339ae39... by Adam Dyess

Read the /etc/cron.d/hwhealth_* tools and allow them to be listed or run from a juju action

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..1dea722 100644
3--- a/src/actions.yaml
4+++ b/src/actions.yaml
5@@ -26,3 +26,13 @@ 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 hwhealth installed cron actions
12+run-cron-action:
13+ description: |
14+ Run the hwhealth cron actions
15+ params:
16+ entries:
17+ type: string
18+ description: Comma sepearated list of cron actions to run (or "all")
19\ No newline at end of file
20diff --git a/src/actions/actions.py b/src/actions/actions.py
21index 4424545..d9cb734 100755
22--- a/src/actions/actions.py
23+++ b/src/actions/actions.py
24@@ -16,6 +16,8 @@
25 """Define hw-health charm actions."""
26
27 import os
28+import pathlib
29+import re
30 import subprocess
31 import sys
32 from datetime import datetime
33@@ -27,6 +29,8 @@ from hwhealth import tools
34
35
36 IPMI_SEL = "/usr/sbin/ipmi-sel"
37+CRON_DIR = pathlib.Path("/etc") / "cron.d"
38+CRON_RE = re.compile(r"\S+ \S+ \S+ \S+ \S+ (\S+) (.*)")
39
40
41 def clear_sel():
42@@ -126,6 +130,48 @@ def unack_sel():
43 action_fail("Action failed with {}".format(e))
44
45
46+def list_cron_actions():
47+ cron_files = CRON_DIR.glob("hwhealth_*")
48+ actions = [path.stem.split("_", 1)[-1] for path in cron_files]
49+ message = "Cron Actions: {}".format(",".join(actions))
50+ log("Action list-cron-actions completed\n" + message)
51+ action_set({"message": message})
52+ return actions
53+
54+
55+def _run_job_file(fname):
56+ with open(fname) as f:
57+ job_lines = f.readlines()
58+ jobs = [
59+ CRON_RE.match(line).groups()
60+ for line in job_lines
61+ if not any(line.startswith(ch) for ch in "# \t")
62+ ]
63+ outs = []
64+ for who, shell in jobs:
65+ cmd = ["sudo", "-u", who, shell]
66+ try:
67+ out = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
68+ except subprocess.CalledProcessError as e:
69+ action_fail("{}: {}\n\t{}".format(who, shell, str(e)))
70+ out = e.output + e.stderr
71+ outs.append((cmd, out))
72+ return outs
73+
74+
75+def run_cron_action():
76+ cron_entries = [entry for entry in action_get("entries").split(",") if entry]
77+ cron_actions = list_cron_actions()
78+ to_run = cron_entries or cron_actions # if nothing specified, run all
79+ results = [
80+ "{}\n\t{}".format(" ".join(cmd), result)
81+ for each in to_run
82+ if each in cron_actions
83+ for cmd, result in _run_job_file(CRON_DIR / "hwhealth-{}".format(each))
84+ ]
85+ action_set({"message": "\n".join(results)})
86+
87+
88 # A dictionary of all the defined actions to callables (which take
89 # parsed arguments).
90 ACTIONS = {
91@@ -133,6 +179,8 @@ ACTIONS = {
92 "show-sel": show_sel,
93 "ack-sel": ack_sel,
94 "unack-sel": unack_sel,
95+ "list-cron-actions": list_cron_actions,
96+ "run-cron-action": run_cron_action,
97 }
98
99
100diff --git a/src/actions/list-cron-actions b/src/actions/list-cron-actions
101new file mode 120000
102index 0000000..405a394
103--- /dev/null
104+++ b/src/actions/list-cron-actions
105@@ -0,0 +1 @@
106+actions.py
107\ No newline at end of file
108diff --git a/src/actions/run-cron-action b/src/actions/run-cron-action
109new file mode 120000
110index 0000000..405a394
111--- /dev/null
112+++ b/src/actions/run-cron-action
113@@ -0,0 +1 @@
114+actions.py
115\ No newline at end of file
116diff --git a/src/tests/unit/test_actions.py b/src/tests/unit/test_actions.py
117index 3a3f4f2..575bbe0 100644
118--- a/src/tests/unit/test_actions.py
119+++ b/src/tests/unit/test_actions.py
120@@ -12,13 +12,21 @@
121 # See the License for the specific language governing permissions and
122 # limitations under the License.
123
124+import pathlib
125 import subprocess
126 import sys
127+import tempfile
128 import unittest
129 import unittest.mock as mock
130
131 sys.path.append(".")
132-from actions.actions import clear_sel, show_sel # noqa:E402
133+from actions.actions import ( # noqa:E402
134+ clear_sel,
135+ list_cron_actions,
136+ _run_job_file,
137+ run_cron_action,
138+ show_sel,
139+)
140
141
142 class ClearSelTestCase(unittest.TestCase):
143@@ -170,3 +178,113 @@ class ShowSelTestCase(unittest.TestCase):
144 "Action failed with Command '['bogus-cmd']' "
145 "returned non-zero exit status 1.",
146 )
147+
148+
149+class ListCronTestCase(unittest.TestCase):
150+ @mock.patch("actions.actions.action_set")
151+ @mock.patch("actions.actions.log")
152+ @mock.patch("actions.actions.CRON_DIR")
153+ def test_list_cron_jobs(self, mock_cron_dir, mock_log, mock_action_set):
154+ mock_cron_dir.glob.return_value = [
155+ pathlib.Path("/path/to/hwhealth_ipmi"),
156+ pathlib.Path("/path/to/hwhealth_test"),
157+ ]
158+
159+ actions = list_cron_actions()
160+ assert actions == ["ipmi", "test"]
161+ mock_cron_dir.glob.assert_called_once_with("hwhealth_*")
162+ ui_response = "Cron Actions: ipmi,test"
163+ mock_log.assert_called_once_with(
164+ "Action list-cron-actions completed\n{}".format(ui_response)
165+ )
166+ mock_action_set.assert_called_once_with({"message": ui_response})
167+
168+ @mock.patch("subprocess.check_output")
169+ def test_run_good_job_file(self, mock_check_output):
170+ with tempfile.TemporaryDirectory() as tempdirname:
171+ test_file = pathlib.Path(tempdirname) / "test"
172+ with open(test_file, "w") as fp:
173+ fp.write(
174+ """
175+# Cron Comments
176+ lines starting with spaces are comments
177+ so are lines starting with tab
178+* * * * * nagios /run/my/command with some --arguments
179+* * * * * root /run/my/command with some --other-arguments
180+""".strip()
181+ )
182+
183+ mock_check_output.side_effect = [
184+ "result #1",
185+ "result #2",
186+ ]
187+ job_output = _run_job_file(test_file)
188+ assert job_output == [
189+ (
190+ ["sudo", "-u", "nagios", "/run/my/command with some --arguments"],
191+ "result #1",
192+ ),
193+ (
194+ [
195+ "sudo",
196+ "-u",
197+ "root",
198+ "/run/my/command with some --other-arguments",
199+ ],
200+ "result #2",
201+ ),
202+ ]
203+
204+ @mock.patch("actions.actions.action_fail")
205+ @mock.patch("subprocess.check_output")
206+ def test_run_job_file_failure(self, mock_check_output, mock_action_fail):
207+ with tempfile.TemporaryDirectory() as tempdirname:
208+ test_file = pathlib.Path(tempdirname) / "test"
209+ with open(test_file, "w") as fp:
210+ fp.write("* * * * * root /run/my/command with some --other-arguments")
211+
212+ mock_check_output.side_effect = [
213+ subprocess.CalledProcessError(
214+ cmd="sudo",
215+ returncode=-1,
216+ output="No ",
217+ stderr="Way",
218+ ),
219+ ]
220+ job_output = _run_job_file(test_file)
221+ assert job_output == [
222+ (
223+ [
224+ "sudo",
225+ "-u",
226+ "root",
227+ "/run/my/command with some --other-arguments",
228+ ],
229+ "No Way",
230+ )
231+ ]
232+ mock_action_fail.assert_called_with(
233+ "root: /run/my/command with some --other-arguments\n\t"
234+ "Command 'sudo' died with <Signals.SIGHUP: 1>."
235+ )
236+
237+ @mock.patch("actions.actions._run_job_file")
238+ @mock.patch("actions.actions.list_cron_actions")
239+ @mock.patch("actions.actions.action_get")
240+ @mock.patch("actions.actions.action_set")
241+ def test_run_cron_action(
242+ self,
243+ mock_action_set,
244+ mock_action_get,
245+ mock_list_cron_actions,
246+ mock_run_job_file,
247+ ):
248+ mock_action_get.return_value = ""
249+ mock_list_cron_actions.return_value = ["test"]
250+ mock_run_job_file.return_value = [
251+ (["sudo", "-u", "root", "/run/my/cmd"], "works!")
252+ ]
253+ run_cron_action()
254+ mock_action_set.assert_called_once_with(
255+ {"message": "sudo -u root /run/my/cmd\n\tworks!"}
256+ )

Subscribers

People subscribed via source and target branches

to all changes: