Skip to content

Commit 3d25aac

Browse files
authored
monit: use enum (#11245)
* monit: use enum * make mypy happy about the var type * add changelog frag * typo - this is getting frequent
1 parent 76589bd commit 3d25aac

File tree

3 files changed

+97
-81
lines changed

3 files changed

+97
-81
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bugfixes:
2+
- monit - internal state was not reflecting when operation is "pending" in ``monit`` (https://github.com/ansible-collections/community.general/pull/11245).
3+
minor_changes:
4+
- monit - use ``Enum`` to represent the possible states (https://github.com/ansible-collections/community.general/pull/11245).

plugins/modules/monit.py

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
import time
5353
import re
5454

55-
from collections import namedtuple
55+
from enum import Enum
5656

5757
from ansible.module_utils.basic import AnsibleModule
5858

@@ -68,38 +68,53 @@
6868
MONIT_SERVICES = ["Process", "File", "Fifo", "Filesystem", "Directory", "Remote host", "System", "Program", "Network"]
6969

7070

71-
class StatusValue(namedtuple("Status", "value, is_pending")):
71+
class StatusValue(Enum):
7272
MISSING = "missing"
7373
OK = "ok"
7474
NOT_MONITORED = "not_monitored"
7575
INITIALIZING = "initializing"
7676
DOES_NOT_EXIST = "does_not_exist"
7777
EXECUTION_FAILED = "execution_failed"
78-
ALL_STATUS = [MISSING, OK, NOT_MONITORED, INITIALIZING, DOES_NOT_EXIST, EXECUTION_FAILED]
7978

80-
def __new__(cls, value, is_pending=False):
81-
return super().__new__(cls, value, is_pending)
79+
80+
class Status:
81+
"""Represents a monit status with optional pending state."""
82+
83+
def __init__(self, status_val: str | StatusValue, is_pending: bool = False):
84+
if isinstance(status_val, StatusValue):
85+
self.state = status_val
86+
else:
87+
self.state = getattr(StatusValue, status_val)
88+
self.is_pending = is_pending
89+
90+
@property
91+
def value(self):
92+
return self.state.value
8293

8394
def pending(self):
84-
return StatusValue(self.value, True)
95+
"""Return a new Status instance with is_pending=True."""
96+
return Status(self.state, is_pending=True)
8597

8698
def __getattr__(self, item):
87-
if item in (f"is_{status}" for status in self.ALL_STATUS):
88-
return self.value == getattr(self, item[3:].upper())
99+
if item.startswith("is_"):
100+
status_name = item[3:].upper()
101+
if hasattr(StatusValue, status_name):
102+
return self.value == getattr(StatusValue, status_name).value
89103
raise AttributeError(item)
90104

105+
def __eq__(self, other):
106+
if not isinstance(other, Status):
107+
return False
108+
return self.state == other.state and self.is_pending == other.is_pending
109+
91110
def __str__(self):
92111
return f"{self.value}{' (pending)' if self.is_pending else ''}"
93112

113+
def __repr__(self):
114+
return f"<{self}>"
94115

95-
class Status:
96-
MISSING = StatusValue(StatusValue.MISSING)
97-
OK = StatusValue(StatusValue.OK)
98-
RUNNING = StatusValue(StatusValue.OK)
99-
NOT_MONITORED = StatusValue(StatusValue.NOT_MONITORED)
100-
INITIALIZING = StatusValue(StatusValue.INITIALIZING)
101-
DOES_NOT_EXIST = StatusValue(StatusValue.DOES_NOT_EXIST)
102-
EXECUTION_FAILED = StatusValue(StatusValue.EXECUTION_FAILED)
116+
117+
# Initialize convenience class attributes
103118

104119

105120
class Monit:
@@ -143,7 +158,7 @@ def exit_success(self, state):
143158
def command_args(self):
144159
return ["-B"] if self.monit_version() > (5, 18) else []
145160

146-
def get_status(self, validate=False):
161+
def get_status(self, validate=False) -> Status:
147162
"""Return the status of the process in monit.
148163
149164
:@param validate: Force monit to re-check the status of the process
@@ -154,35 +169,38 @@ def get_status(self, validate=False):
154169
rc, out, err = self.module.run_command(command, check_rc=check_rc)
155170
return self._parse_status(out, err)
156171

157-
def _parse_status(self, output, err):
172+
def _parse_status(self, output, err) -> Status:
158173
escaped_monit_services = "|".join([re.escape(x) for x in MONIT_SERVICES])
159174
pattern = f"({escaped_monit_services}) '{re.escape(self.process_name)}'"
160175
if not re.search(pattern, output, re.IGNORECASE):
161-
return Status.MISSING
176+
return Status("MISSING")
162177

163-
status_val = re.findall(r"^\s*status\s*([\w\- ]+)", output, re.MULTILINE)
164-
if not status_val:
178+
status_val_find = re.findall(r"^\s*status\s*([\w\- ]+)", output, re.MULTILINE)
179+
if not status_val_find:
165180
self.exit_fail("Unable to find process status", stdout=output, stderr=err)
166181

167-
status_val = status_val[0].strip().upper()
182+
status_val = status_val_find[0].strip().upper()
168183
if " | " in status_val:
169184
status_val = status_val.split(" | ")[0]
170185
if " - " not in status_val:
171186
status_val = status_val.replace(" ", "_")
187+
# Normalize RUNNING to OK (monit reports both, they mean the same thing)
188+
if status_val == "RUNNING":
189+
status_val = "OK"
172190
try:
173-
return getattr(Status, status_val)
191+
return Status(status_val)
174192
except AttributeError:
175193
self.module.warn(f"Unknown monit status '{status_val}', treating as execution failed")
176-
return Status.EXECUTION_FAILED
194+
return Status("EXECUTION_FAILED")
177195
else:
178196
status_val, substatus = status_val.split(" - ")
179197
action, state = substatus.split()
180198
if action in ["START", "INITIALIZING", "RESTART", "MONITOR"]:
181-
status = Status.OK
199+
status = Status("OK")
182200
else:
183-
status = Status.NOT_MONITORED
201+
status = Status("NOT_MONITORED")
184202

185-
if state == "pending":
203+
if state == "PENDING":
186204
status = status.pending()
187205
return status
188206

@@ -225,7 +243,7 @@ def wait_for_monit_to_stop_pending(self, current_status=None):
225243
StatusValue.INITIALIZING,
226244
StatusValue.DOES_NOT_EXIST,
227245
]
228-
while current_status.is_pending or (current_status.value in waiting_status):
246+
while current_status.is_pending or (current_status.state in waiting_status):
229247
if time.time() >= timeout_time:
230248
self.exit_fail('waited too long for "pending", or "initiating" status to go away', current_status)
231249

@@ -251,32 +269,32 @@ def present(self):
251269

252270
self.exit_success(state="present")
253271

254-
def change_state(self, state, expected_status, invert_expected=None):
272+
def change_state(self, state: str, expected_status: StatusValue, invert_expected: bool | None = None):
255273
current_status = self.get_status()
256274
self.run_command(STATE_COMMAND_MAP[state])
257275
status = self.wait_for_status_change(current_status)
258276
status = self.wait_for_monit_to_stop_pending(status)
259-
status_match = status.value == expected_status.value
277+
status_match = status.state == expected_status
260278
if invert_expected:
261279
status_match = not status_match
262280
if status_match:
263281
self.exit_success(state=state)
264282
self.exit_fail(f"{self.process_name} process not {state}", status)
265283

266284
def stop(self):
267-
self.change_state("stopped", Status.NOT_MONITORED)
285+
self.change_state("stopped", expected_status=StatusValue.NOT_MONITORED)
268286

269287
def unmonitor(self):
270-
self.change_state("unmonitored", Status.NOT_MONITORED)
288+
self.change_state("unmonitored", expected_status=StatusValue.NOT_MONITORED)
271289

272290
def restart(self):
273-
self.change_state("restarted", Status.OK)
291+
self.change_state("restarted", expected_status=StatusValue.OK)
274292

275293
def start(self):
276-
self.change_state("started", Status.OK)
294+
self.change_state("started", expected_status=StatusValue.OK)
277295

278296
def monitor(self):
279-
self.change_state("monitored", Status.NOT_MONITORED, invert_expected=True)
297+
self.change_state("monitored", expected_status=StatusValue.NOT_MONITORED, invert_expected=True)
280298

281299

282300
def main():

tests/unit/plugins/modules/test_monit.py

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ def patch_status(self, side_effect):
4141
return mock.patch.object(self.monit, "get_status", side_effect=side_effect)
4242

4343
def test_change_state_success(self):
44-
with self.patch_status([monit.Status.OK, monit.Status.NOT_MONITORED]):
44+
with self.patch_status([monit.Status("OK"), monit.Status("NOT_MONITORED")]):
4545
with self.assertRaises(AnsibleExitJson):
4646
self.monit.stop()
4747
self.module.fail_json.assert_not_called()
4848
self.module.run_command.assert_called_with(["monit", "stop", "processX"], check_rc=True)
4949

5050
def test_change_state_fail(self):
51-
with self.patch_status([monit.Status.OK] * 3):
51+
with self.patch_status([monit.Status("OK")] * 3):
5252
with self.assertRaises(AnsibleFailJson):
5353
self.monit.stop()
5454

@@ -59,78 +59,72 @@ def test_reload_fail(self):
5959

6060
def test_reload(self):
6161
self.module.run_command.return_value = (0, "", "")
62-
with self.patch_status(monit.Status.OK):
62+
with self.patch_status(monit.Status("OK")):
6363
with self.assertRaises(AnsibleExitJson):
6464
self.monit.reload()
6565

6666
def test_wait_for_status_to_stop_pending(self):
6767
status = [
68-
monit.Status.MISSING,
69-
monit.Status.DOES_NOT_EXIST,
70-
monit.Status.INITIALIZING,
71-
monit.Status.OK.pending(),
72-
monit.Status.OK,
68+
monit.Status("MISSING"),
69+
monit.Status("DOES_NOT_EXIST"),
70+
monit.Status("INITIALIZING"),
71+
monit.Status("OK").pending(),
72+
monit.Status("OK"),
7373
]
7474
with self.patch_status(status) as get_status:
7575
self.monit.wait_for_monit_to_stop_pending()
7676
self.assertEqual(get_status.call_count, len(status))
7777

7878
def test_wait_for_status_change(self):
79-
with self.patch_status([monit.Status.NOT_MONITORED, monit.Status.OK]) as get_status:
80-
self.monit.wait_for_status_change(monit.Status.NOT_MONITORED)
79+
with self.patch_status([monit.Status("NOT_MONITORED"), monit.Status("OK")]) as get_status:
80+
self.monit.wait_for_status_change(monit.Status("NOT_MONITORED"))
8181
self.assertEqual(get_status.call_count, 2)
8282

8383
def test_wait_for_status_change_fail(self):
84-
with self.patch_status([monit.Status.OK] * 3):
84+
with self.patch_status([monit.Status("OK")] * 3):
8585
with self.assertRaises(AnsibleFailJson):
86-
self.monit.wait_for_status_change(monit.Status.OK)
86+
self.monit.wait_for_status_change(monit.Status("OK"))
8787

8888
def test_monitor(self):
89-
with self.patch_status([monit.Status.NOT_MONITORED, monit.Status.OK.pending(), monit.Status.OK]):
89+
with self.patch_status([monit.Status("NOT_MONITORED"), monit.Status("OK").pending(), monit.Status("OK")]):
9090
with self.assertRaises(AnsibleExitJson):
9191
self.monit.monitor()
9292

9393
def test_monitor_fail(self):
94-
with self.patch_status([monit.Status.NOT_MONITORED] * 3):
94+
with self.patch_status([monit.Status("NOT_MONITORED")] * 3):
9595
with self.assertRaises(AnsibleFailJson):
9696
self.monit.monitor()
9797

9898
def test_timeout(self):
9999
self.monit.timeout = 0
100-
with self.patch_status(monit.Status.NOT_MONITORED.pending()):
100+
with self.patch_status(monit.Status("NOT_MONITORED").pending()):
101101
with self.assertRaises(AnsibleFailJson):
102102
self.monit.wait_for_monit_to_stop_pending()
103103

104104

105-
@pytest.mark.parametrize("status_name", monit.StatusValue.ALL_STATUS)
106-
def test_status_value(status_name):
107-
value = getattr(monit.StatusValue, status_name.upper())
108-
status = monit.StatusValue(value)
109-
assert getattr(status, f"is_{status_name}")
110-
assert not all(getattr(status, f"is_{name}") for name in monit.StatusValue.ALL_STATUS if name != status_name)
111-
112-
113105
BASIC_OUTPUT_CASES = [
114-
(TEST_OUTPUT % ("Process", "processX", name), getattr(monit.Status, name.upper()))
115-
for name in monit.StatusValue.ALL_STATUS
106+
(TEST_OUTPUT % ("Process", "processX", member.value), monit.Status(member.name)) for member in monit.StatusValue
116107
]
117108

118109

119110
@pytest.mark.parametrize(
120111
"output, expected",
121112
BASIC_OUTPUT_CASES
122113
+ [
123-
("", monit.Status.MISSING),
124-
(TEST_OUTPUT % ("Process", "processY", "OK"), monit.Status.MISSING),
125-
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - start pending"), monit.Status.OK),
126-
(TEST_OUTPUT % ("Process", "processX", "Monitored - stop pending"), monit.Status.NOT_MONITORED),
127-
(TEST_OUTPUT % ("Process", "processX", "Monitored - restart pending"), monit.Status.OK),
128-
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - monitor pending"), monit.Status.OK),
129-
(TEST_OUTPUT % ("Process", "processX", "Does not exist"), monit.Status.DOES_NOT_EXIST),
130-
(TEST_OUTPUT % ("Process", "processX", "Not monitored"), monit.Status.NOT_MONITORED),
131-
(TEST_OUTPUT % ("Process", "processX", "Running"), monit.Status.OK),
132-
(TEST_OUTPUT % ("Process", "processX", "Execution failed | Does not exist"), monit.Status.EXECUTION_FAILED),
133-
(TEST_OUTPUT % ("Process", "processX", "Some Unknown Status"), monit.Status.EXECUTION_FAILED),
114+
("", monit.Status("MISSING")),
115+
(TEST_OUTPUT % ("Process", "processY", "OK"), monit.Status("MISSING")),
116+
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - start pending"), monit.Status("OK", is_pending=True)),
117+
(
118+
TEST_OUTPUT % ("Process", "processX", "Monitored - stop pending"),
119+
monit.Status("NOT_MONITORED", is_pending=True),
120+
),
121+
(TEST_OUTPUT % ("Process", "processX", "Monitored - restart pending"), monit.Status("OK", is_pending=True)),
122+
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - monitor pending"), monit.Status("OK", is_pending=True)),
123+
(TEST_OUTPUT % ("Process", "processX", "Does not exist"), monit.Status("DOES_NOT_EXIST")),
124+
(TEST_OUTPUT % ("Process", "processX", "Not monitored"), monit.Status("NOT_MONITORED")),
125+
(TEST_OUTPUT % ("Process", "processX", "Running"), monit.Status("OK")),
126+
(TEST_OUTPUT % ("Process", "processX", "Execution failed | Does not exist"), monit.Status("EXECUTION_FAILED")),
127+
(TEST_OUTPUT % ("Process", "processX", "Some Unknown Status"), monit.Status("EXECUTION_FAILED")),
134128
],
135129
)
136130
def test_parse_status(output, expected):
@@ -143,16 +137,16 @@ def test_parse_status(output, expected):
143137
"output, expected",
144138
BASIC_OUTPUT_CASES
145139
+ [
146-
(TEST_OUTPUT % ("Process", "processX", "OK"), monit.Status.OK),
147-
(TEST_OUTPUT % ("File", "processX", "OK"), monit.Status.OK),
148-
(TEST_OUTPUT % ("Fifo", "processX", "OK"), monit.Status.OK),
149-
(TEST_OUTPUT % ("Filesystem", "processX", "OK"), monit.Status.OK),
150-
(TEST_OUTPUT % ("Directory", "processX", "OK"), monit.Status.OK),
151-
(TEST_OUTPUT % ("Remote host", "processX", "OK"), monit.Status.OK),
152-
(TEST_OUTPUT % ("System", "processX", "OK"), monit.Status.OK),
153-
(TEST_OUTPUT % ("Program", "processX", "OK"), monit.Status.OK),
154-
(TEST_OUTPUT % ("Network", "processX", "OK"), monit.Status.OK),
155-
(TEST_OUTPUT % ("Unsupported", "processX", "OK"), monit.Status.MISSING),
140+
(TEST_OUTPUT % ("Process", "processX", "OK"), monit.Status("OK")),
141+
(TEST_OUTPUT % ("File", "processX", "OK"), monit.Status("OK")),
142+
(TEST_OUTPUT % ("Fifo", "processX", "OK"), monit.Status("OK")),
143+
(TEST_OUTPUT % ("Filesystem", "processX", "OK"), monit.Status("OK")),
144+
(TEST_OUTPUT % ("Directory", "processX", "OK"), monit.Status("OK")),
145+
(TEST_OUTPUT % ("Remote host", "processX", "OK"), monit.Status("OK")),
146+
(TEST_OUTPUT % ("System", "processX", "OK"), monit.Status("OK")),
147+
(TEST_OUTPUT % ("Program", "processX", "OK"), monit.Status("OK")),
148+
(TEST_OUTPUT % ("Network", "processX", "OK"), monit.Status("OK")),
149+
(TEST_OUTPUT % ("Unsupported", "processX", "OK"), monit.Status("MISSING")),
156150
],
157151
)
158152
def test_parse_status_supports_all_services(output, expected):

0 commit comments

Comments
 (0)