1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 07:51:50 +00:00

[PR #11245/3d25aac9 backport][stable-12] monit: use enum (#11252)

monit: use enum (#11245)

* monit: use enum

* make mypy happy about the var type

* add changelog frag

* typo - this is getting frequent

(cherry picked from commit 3d25aac978)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
This commit is contained in:
patchback[bot] 2025-12-02 22:18:19 +01:00 committed by GitHub
parent ee2963d1ee
commit 7309650d26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 97 additions and 81 deletions

View file

@ -0,0 +1,4 @@
bugfixes:
- monit - internal state was not reflecting when operation is "pending" in ``monit`` (https://github.com/ansible-collections/community.general/pull/11245).
minor_changes:
- monit - use ``Enum`` to represent the possible states (https://github.com/ansible-collections/community.general/pull/11245).

View file

@ -52,7 +52,7 @@ EXAMPLES = r"""
import time
import re
from collections import namedtuple
from enum import Enum
from ansible.module_utils.basic import AnsibleModule
@ -68,38 +68,53 @@ STATE_COMMAND_MAP = {
MONIT_SERVICES = ["Process", "File", "Fifo", "Filesystem", "Directory", "Remote host", "System", "Program", "Network"]
class StatusValue(namedtuple("Status", "value, is_pending")):
class StatusValue(Enum):
MISSING = "missing"
OK = "ok"
NOT_MONITORED = "not_monitored"
INITIALIZING = "initializing"
DOES_NOT_EXIST = "does_not_exist"
EXECUTION_FAILED = "execution_failed"
ALL_STATUS = [MISSING, OK, NOT_MONITORED, INITIALIZING, DOES_NOT_EXIST, EXECUTION_FAILED]
def __new__(cls, value, is_pending=False):
return super().__new__(cls, value, is_pending)
class Status:
"""Represents a monit status with optional pending state."""
def __init__(self, status_val: str | StatusValue, is_pending: bool = False):
if isinstance(status_val, StatusValue):
self.state = status_val
else:
self.state = getattr(StatusValue, status_val)
self.is_pending = is_pending
@property
def value(self):
return self.state.value
def pending(self):
return StatusValue(self.value, True)
"""Return a new Status instance with is_pending=True."""
return Status(self.state, is_pending=True)
def __getattr__(self, item):
if item in (f"is_{status}" for status in self.ALL_STATUS):
return self.value == getattr(self, item[3:].upper())
if item.startswith("is_"):
status_name = item[3:].upper()
if hasattr(StatusValue, status_name):
return self.value == getattr(StatusValue, status_name).value
raise AttributeError(item)
def __eq__(self, other):
if not isinstance(other, Status):
return False
return self.state == other.state and self.is_pending == other.is_pending
def __str__(self):
return f"{self.value}{' (pending)' if self.is_pending else ''}"
def __repr__(self):
return f"<{self}>"
class Status:
MISSING = StatusValue(StatusValue.MISSING)
OK = StatusValue(StatusValue.OK)
RUNNING = StatusValue(StatusValue.OK)
NOT_MONITORED = StatusValue(StatusValue.NOT_MONITORED)
INITIALIZING = StatusValue(StatusValue.INITIALIZING)
DOES_NOT_EXIST = StatusValue(StatusValue.DOES_NOT_EXIST)
EXECUTION_FAILED = StatusValue(StatusValue.EXECUTION_FAILED)
# Initialize convenience class attributes
class Monit:
@ -143,7 +158,7 @@ class Monit:
def command_args(self):
return ["-B"] if self.monit_version() > (5, 18) else []
def get_status(self, validate=False):
def get_status(self, validate=False) -> Status:
"""Return the status of the process in monit.
:@param validate: Force monit to re-check the status of the process
@ -154,35 +169,38 @@ class Monit:
rc, out, err = self.module.run_command(command, check_rc=check_rc)
return self._parse_status(out, err)
def _parse_status(self, output, err):
def _parse_status(self, output, err) -> Status:
escaped_monit_services = "|".join([re.escape(x) for x in MONIT_SERVICES])
pattern = f"({escaped_monit_services}) '{re.escape(self.process_name)}'"
if not re.search(pattern, output, re.IGNORECASE):
return Status.MISSING
return Status("MISSING")
status_val = re.findall(r"^\s*status\s*([\w\- ]+)", output, re.MULTILINE)
if not status_val:
status_val_find = re.findall(r"^\s*status\s*([\w\- ]+)", output, re.MULTILINE)
if not status_val_find:
self.exit_fail("Unable to find process status", stdout=output, stderr=err)
status_val = status_val[0].strip().upper()
status_val = status_val_find[0].strip().upper()
if " | " in status_val:
status_val = status_val.split(" | ")[0]
if " - " not in status_val:
status_val = status_val.replace(" ", "_")
# Normalize RUNNING to OK (monit reports both, they mean the same thing)
if status_val == "RUNNING":
status_val = "OK"
try:
return getattr(Status, status_val)
return Status(status_val)
except AttributeError:
self.module.warn(f"Unknown monit status '{status_val}', treating as execution failed")
return Status.EXECUTION_FAILED
return Status("EXECUTION_FAILED")
else:
status_val, substatus = status_val.split(" - ")
action, state = substatus.split()
if action in ["START", "INITIALIZING", "RESTART", "MONITOR"]:
status = Status.OK
status = Status("OK")
else:
status = Status.NOT_MONITORED
status = Status("NOT_MONITORED")
if state == "pending":
if state == "PENDING":
status = status.pending()
return status
@ -225,7 +243,7 @@ class Monit:
StatusValue.INITIALIZING,
StatusValue.DOES_NOT_EXIST,
]
while current_status.is_pending or (current_status.value in waiting_status):
while current_status.is_pending or (current_status.state in waiting_status):
if time.time() >= timeout_time:
self.exit_fail('waited too long for "pending", or "initiating" status to go away', current_status)
@ -251,12 +269,12 @@ class Monit:
self.exit_success(state="present")
def change_state(self, state, expected_status, invert_expected=None):
def change_state(self, state: str, expected_status: StatusValue, invert_expected: bool | None = None):
current_status = self.get_status()
self.run_command(STATE_COMMAND_MAP[state])
status = self.wait_for_status_change(current_status)
status = self.wait_for_monit_to_stop_pending(status)
status_match = status.value == expected_status.value
status_match = status.state == expected_status
if invert_expected:
status_match = not status_match
if status_match:
@ -264,19 +282,19 @@ class Monit:
self.exit_fail(f"{self.process_name} process not {state}", status)
def stop(self):
self.change_state("stopped", Status.NOT_MONITORED)
self.change_state("stopped", expected_status=StatusValue.NOT_MONITORED)
def unmonitor(self):
self.change_state("unmonitored", Status.NOT_MONITORED)
self.change_state("unmonitored", expected_status=StatusValue.NOT_MONITORED)
def restart(self):
self.change_state("restarted", Status.OK)
self.change_state("restarted", expected_status=StatusValue.OK)
def start(self):
self.change_state("started", Status.OK)
self.change_state("started", expected_status=StatusValue.OK)
def monitor(self):
self.change_state("monitored", Status.NOT_MONITORED, invert_expected=True)
self.change_state("monitored", expected_status=StatusValue.NOT_MONITORED, invert_expected=True)
def main():

View file

@ -41,14 +41,14 @@ class MonitTest(unittest.TestCase):
return mock.patch.object(self.monit, "get_status", side_effect=side_effect)
def test_change_state_success(self):
with self.patch_status([monit.Status.OK, monit.Status.NOT_MONITORED]):
with self.patch_status([monit.Status("OK"), monit.Status("NOT_MONITORED")]):
with self.assertRaises(AnsibleExitJson):
self.monit.stop()
self.module.fail_json.assert_not_called()
self.module.run_command.assert_called_with(["monit", "stop", "processX"], check_rc=True)
def test_change_state_fail(self):
with self.patch_status([monit.Status.OK] * 3):
with self.patch_status([monit.Status("OK")] * 3):
with self.assertRaises(AnsibleFailJson):
self.monit.stop()
@ -59,60 +59,51 @@ class MonitTest(unittest.TestCase):
def test_reload(self):
self.module.run_command.return_value = (0, "", "")
with self.patch_status(monit.Status.OK):
with self.patch_status(monit.Status("OK")):
with self.assertRaises(AnsibleExitJson):
self.monit.reload()
def test_wait_for_status_to_stop_pending(self):
status = [
monit.Status.MISSING,
monit.Status.DOES_NOT_EXIST,
monit.Status.INITIALIZING,
monit.Status.OK.pending(),
monit.Status.OK,
monit.Status("MISSING"),
monit.Status("DOES_NOT_EXIST"),
monit.Status("INITIALIZING"),
monit.Status("OK").pending(),
monit.Status("OK"),
]
with self.patch_status(status) as get_status:
self.monit.wait_for_monit_to_stop_pending()
self.assertEqual(get_status.call_count, len(status))
def test_wait_for_status_change(self):
with self.patch_status([monit.Status.NOT_MONITORED, monit.Status.OK]) as get_status:
self.monit.wait_for_status_change(monit.Status.NOT_MONITORED)
with self.patch_status([monit.Status("NOT_MONITORED"), monit.Status("OK")]) as get_status:
self.monit.wait_for_status_change(monit.Status("NOT_MONITORED"))
self.assertEqual(get_status.call_count, 2)
def test_wait_for_status_change_fail(self):
with self.patch_status([monit.Status.OK] * 3):
with self.patch_status([monit.Status("OK")] * 3):
with self.assertRaises(AnsibleFailJson):
self.monit.wait_for_status_change(monit.Status.OK)
self.monit.wait_for_status_change(monit.Status("OK"))
def test_monitor(self):
with self.patch_status([monit.Status.NOT_MONITORED, monit.Status.OK.pending(), monit.Status.OK]):
with self.patch_status([monit.Status("NOT_MONITORED"), monit.Status("OK").pending(), monit.Status("OK")]):
with self.assertRaises(AnsibleExitJson):
self.monit.monitor()
def test_monitor_fail(self):
with self.patch_status([monit.Status.NOT_MONITORED] * 3):
with self.patch_status([monit.Status("NOT_MONITORED")] * 3):
with self.assertRaises(AnsibleFailJson):
self.monit.monitor()
def test_timeout(self):
self.monit.timeout = 0
with self.patch_status(monit.Status.NOT_MONITORED.pending()):
with self.patch_status(monit.Status("NOT_MONITORED").pending()):
with self.assertRaises(AnsibleFailJson):
self.monit.wait_for_monit_to_stop_pending()
@pytest.mark.parametrize("status_name", monit.StatusValue.ALL_STATUS)
def test_status_value(status_name):
value = getattr(monit.StatusValue, status_name.upper())
status = monit.StatusValue(value)
assert getattr(status, f"is_{status_name}")
assert not all(getattr(status, f"is_{name}") for name in monit.StatusValue.ALL_STATUS if name != status_name)
BASIC_OUTPUT_CASES = [
(TEST_OUTPUT % ("Process", "processX", name), getattr(monit.Status, name.upper()))
for name in monit.StatusValue.ALL_STATUS
(TEST_OUTPUT % ("Process", "processX", member.value), monit.Status(member.name)) for member in monit.StatusValue
]
@ -120,17 +111,20 @@ BASIC_OUTPUT_CASES = [
"output, expected",
BASIC_OUTPUT_CASES
+ [
("", monit.Status.MISSING),
(TEST_OUTPUT % ("Process", "processY", "OK"), monit.Status.MISSING),
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - start pending"), monit.Status.OK),
(TEST_OUTPUT % ("Process", "processX", "Monitored - stop pending"), monit.Status.NOT_MONITORED),
(TEST_OUTPUT % ("Process", "processX", "Monitored - restart pending"), monit.Status.OK),
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - monitor pending"), monit.Status.OK),
(TEST_OUTPUT % ("Process", "processX", "Does not exist"), monit.Status.DOES_NOT_EXIST),
(TEST_OUTPUT % ("Process", "processX", "Not monitored"), monit.Status.NOT_MONITORED),
(TEST_OUTPUT % ("Process", "processX", "Running"), monit.Status.OK),
(TEST_OUTPUT % ("Process", "processX", "Execution failed | Does not exist"), monit.Status.EXECUTION_FAILED),
(TEST_OUTPUT % ("Process", "processX", "Some Unknown Status"), monit.Status.EXECUTION_FAILED),
("", monit.Status("MISSING")),
(TEST_OUTPUT % ("Process", "processY", "OK"), monit.Status("MISSING")),
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - start pending"), monit.Status("OK", is_pending=True)),
(
TEST_OUTPUT % ("Process", "processX", "Monitored - stop pending"),
monit.Status("NOT_MONITORED", is_pending=True),
),
(TEST_OUTPUT % ("Process", "processX", "Monitored - restart pending"), monit.Status("OK", is_pending=True)),
(TEST_OUTPUT % ("Process", "processX", "Not Monitored - monitor pending"), monit.Status("OK", is_pending=True)),
(TEST_OUTPUT % ("Process", "processX", "Does not exist"), monit.Status("DOES_NOT_EXIST")),
(TEST_OUTPUT % ("Process", "processX", "Not monitored"), monit.Status("NOT_MONITORED")),
(TEST_OUTPUT % ("Process", "processX", "Running"), monit.Status("OK")),
(TEST_OUTPUT % ("Process", "processX", "Execution failed | Does not exist"), monit.Status("EXECUTION_FAILED")),
(TEST_OUTPUT % ("Process", "processX", "Some Unknown Status"), monit.Status("EXECUTION_FAILED")),
],
)
def test_parse_status(output, expected):
@ -143,16 +137,16 @@ def test_parse_status(output, expected):
"output, expected",
BASIC_OUTPUT_CASES
+ [
(TEST_OUTPUT % ("Process", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("File", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("Fifo", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("Filesystem", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("Directory", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("Remote host", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("System", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("Program", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("Network", "processX", "OK"), monit.Status.OK),
(TEST_OUTPUT % ("Unsupported", "processX", "OK"), monit.Status.MISSING),
(TEST_OUTPUT % ("Process", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("File", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("Fifo", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("Filesystem", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("Directory", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("Remote host", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("System", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("Program", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("Network", "processX", "OK"), monit.Status("OK")),
(TEST_OUTPUT % ("Unsupported", "processX", "OK"), monit.Status("MISSING")),
],
)
def test_parse_status_supports_all_services(output, expected):