1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# (c) 2013, Darryl Stoflet <stoflet@gmail.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = '''
12---
13module: monit
14short_description: Manage the state of a program monitored via Monit
15description:
16    - Manage the state of a program monitored via I(Monit).
17options:
18  name:
19    description:
20      - The name of the I(monit) program/process to manage.
21    required: true
22    type: str
23  state:
24    description:
25      - The state of service.
26    required: true
27    choices: [ "present", "started", "stopped", "restarted", "monitored", "unmonitored", "reloaded" ]
28    type: str
29  timeout:
30    description:
31      - If there are pending actions for the service monitored by monit, then Ansible will check
32        for up to this many seconds to verify the requested action has been performed.
33        Ansible will sleep for five seconds between each check.
34    default: 300
35    type: int
36author:
37    - Darryl Stoflet (@dstoflet)
38    - Simon Kelly (@snopoke)
39'''
40
41EXAMPLES = '''
42- name: Manage the state of program httpd to be in started state
43  community.general.monit:
44    name: httpd
45    state: started
46'''
47
48import time
49import re
50
51from collections import namedtuple
52
53from ansible.module_utils.basic import AnsibleModule
54from ansible.module_utils.six import python_2_unicode_compatible
55
56
57STATE_COMMAND_MAP = {
58    'stopped': 'stop',
59    'started': 'start',
60    'monitored': 'monitor',
61    'unmonitored': 'unmonitor',
62    'restarted': 'restart'
63}
64
65MONIT_SERVICES = ['Process', 'File', 'Fifo', 'Filesystem', 'Directory', 'Remote host', 'System', 'Program',
66                  'Network']
67
68
69@python_2_unicode_compatible
70class StatusValue(namedtuple("Status", "value, is_pending")):
71    MISSING = 'missing'
72    OK = 'ok'
73    NOT_MONITORED = 'not_monitored'
74    INITIALIZING = 'initializing'
75    DOES_NOT_EXIST = 'does_not_exist'
76    EXECUTION_FAILED = 'execution_failed'
77    ALL_STATUS = [
78        MISSING, OK, NOT_MONITORED, INITIALIZING, DOES_NOT_EXIST, EXECUTION_FAILED
79    ]
80
81    def __new__(cls, value, is_pending=False):
82        return super(StatusValue, cls).__new__(cls, value, is_pending)
83
84    def pending(self):
85        return StatusValue(self.value, True)
86
87    def __getattr__(self, item):
88        if item in ('is_%s' % status for status in self.ALL_STATUS):
89            return self.value == getattr(self, item[3:].upper())
90        raise AttributeError(item)
91
92    def __str__(self):
93        return "%s%s" % (self.value, " (pending)" if self.is_pending else "")
94
95
96class Status(object):
97    MISSING = StatusValue(StatusValue.MISSING)
98    OK = StatusValue(StatusValue.OK)
99    RUNNING = StatusValue(StatusValue.OK)
100    NOT_MONITORED = StatusValue(StatusValue.NOT_MONITORED)
101    INITIALIZING = StatusValue(StatusValue.INITIALIZING)
102    DOES_NOT_EXIST = StatusValue(StatusValue.DOES_NOT_EXIST)
103    EXECUTION_FAILED = StatusValue(StatusValue.EXECUTION_FAILED)
104
105
106class Monit(object):
107    def __init__(self, module, monit_bin_path, service_name, timeout):
108        self.module = module
109        self.monit_bin_path = monit_bin_path
110        self.process_name = service_name
111        self.timeout = timeout
112
113        self._monit_version = None
114        self._raw_version = None
115        self._status_change_retry_count = 6
116
117    def monit_version(self):
118        if self._monit_version is None:
119            self._raw_version, version = self._get_monit_version()
120            # Use only major and minor even if there are more these should be enough
121            self._monit_version = version[0], version[1]
122        return self._monit_version
123
124    def _get_monit_version(self):
125        rc, out, err = self.module.run_command('%s -V' % self.monit_bin_path, check_rc=True)
126        version_line = out.split('\n')[0]
127        raw_version = re.search(r"([0-9]+\.){1,2}([0-9]+)?", version_line).group()
128        return raw_version, tuple(map(int, raw_version.split('.')))
129
130    def exit_fail(self, msg, status=None, **kwargs):
131        kwargs.update({
132            'msg': msg,
133            'monit_version': self._raw_version,
134            'process_status': str(status) if status else None,
135        })
136        self.module.fail_json(**kwargs)
137
138    def exit_success(self, state):
139        self.module.exit_json(changed=True, name=self.process_name, state=state)
140
141    @property
142    def command_args(self):
143        return "-B" if self.monit_version() > (5, 18) else ""
144
145    def get_status(self, validate=False):
146        """Return the status of the process in monit.
147
148        :@param validate: Force monit to re-check the status of the process
149        """
150        monit_command = "validate" if validate else "status"
151        check_rc = False if validate else True  # 'validate' always has rc = 1
152        command = ' '.join([self.monit_bin_path, monit_command, self.command_args, self.process_name])
153        rc, out, err = self.module.run_command(command, check_rc=check_rc)
154        return self._parse_status(out, err)
155
156    def _parse_status(self, output, err):
157        escaped_monit_services = '|'.join([re.escape(x) for x in MONIT_SERVICES])
158        pattern = "(%s) '%s'" % (escaped_monit_services, re.escape(self.process_name))
159        if not re.search(pattern, output, re.IGNORECASE):
160            return Status.MISSING
161
162        status_val = re.findall(r"^\s*status\s*([\w\- ]+)", output, re.MULTILINE)
163        if not status_val:
164            self.exit_fail("Unable to find process status", stdout=output, stderr=err)
165
166        status_val = status_val[0].strip().upper()
167        if ' | ' in status_val:
168            status_val = status_val.split(' | ')[0]
169        if ' - ' not in status_val:
170            status_val = status_val.replace(' ', '_')
171            return getattr(Status, status_val)
172        else:
173            status_val, substatus = status_val.split(' - ')
174            action, state = substatus.split()
175            if action in ['START', 'INITIALIZING', 'RESTART', 'MONITOR']:
176                status = Status.OK
177            else:
178                status = Status.NOT_MONITORED
179
180            if state == 'pending':
181                status = status.pending()
182            return status
183
184    def is_process_present(self):
185        rc, out, err = self.module.run_command('%s summary %s' % (self.monit_bin_path, self.command_args), check_rc=True)
186        return bool(re.findall(r'\b%s\b' % self.process_name, out))
187
188    def is_process_running(self):
189        return self.get_status().is_ok
190
191    def run_command(self, command):
192        """Runs a monit command, and returns the new status."""
193        return self.module.run_command('%s %s %s' % (self.monit_bin_path, command, self.process_name), check_rc=True)
194
195    def wait_for_status_change(self, current_status):
196        running_status = self.get_status()
197        if running_status.value != current_status.value or current_status.value == StatusValue.EXECUTION_FAILED:
198            return running_status
199
200        loop_count = 0
201        while running_status.value == current_status.value:
202            if loop_count >= self._status_change_retry_count:
203                self.exit_fail('waited too long for monit to change state', running_status)
204
205            loop_count += 1
206            time.sleep(0.5)
207            validate = loop_count % 2 == 0  # force recheck of status every second try
208            running_status = self.get_status(validate)
209        return running_status
210
211    def wait_for_monit_to_stop_pending(self, current_status=None):
212        """Fails this run if there is no status or it's pending/initializing for timeout"""
213        timeout_time = time.time() + self.timeout
214
215        if not current_status:
216            current_status = self.get_status()
217        waiting_status = [
218            StatusValue.MISSING,
219            StatusValue.INITIALIZING,
220            StatusValue.DOES_NOT_EXIST,
221        ]
222        while current_status.is_pending or (current_status.value in waiting_status):
223            if time.time() >= timeout_time:
224                self.exit_fail('waited too long for "pending", or "initiating" status to go away', current_status)
225
226            time.sleep(5)
227            current_status = self.get_status(validate=True)
228        return current_status
229
230    def reload(self):
231        rc, out, err = self.module.run_command('%s reload' % self.monit_bin_path)
232        if rc != 0:
233            self.exit_fail('monit reload failed', stdout=out, stderr=err)
234        self.exit_success(state='reloaded')
235
236    def present(self):
237        self.run_command('reload')
238
239        timeout_time = time.time() + self.timeout
240        while not self.is_process_present():
241            if time.time() >= timeout_time:
242                self.exit_fail('waited too long for process to become "present"')
243
244            time.sleep(5)
245
246        self.exit_success(state='present')
247
248    def change_state(self, state, expected_status, invert_expected=None):
249        current_status = self.get_status()
250        self.run_command(STATE_COMMAND_MAP[state])
251        status = self.wait_for_status_change(current_status)
252        status = self.wait_for_monit_to_stop_pending(status)
253        status_match = status.value == expected_status.value
254        if invert_expected:
255            status_match = not status_match
256        if status_match:
257            self.exit_success(state=state)
258        self.exit_fail('%s process not %s' % (self.process_name, state), status)
259
260    def stop(self):
261        self.change_state('stopped', Status.NOT_MONITORED)
262
263    def unmonitor(self):
264        self.change_state('unmonitored', Status.NOT_MONITORED)
265
266    def restart(self):
267        self.change_state('restarted', Status.OK)
268
269    def start(self):
270        self.change_state('started', Status.OK)
271
272    def monitor(self):
273        self.change_state('monitored', Status.NOT_MONITORED, invert_expected=True)
274
275
276def main():
277    arg_spec = dict(
278        name=dict(required=True),
279        timeout=dict(default=300, type='int'),
280        state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped', 'monitored', 'unmonitored', 'reloaded'])
281    )
282
283    module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True)
284
285    name = module.params['name']
286    state = module.params['state']
287    timeout = module.params['timeout']
288
289    monit = Monit(module, module.get_bin_path('monit', True), name, timeout)
290
291    def exit_if_check_mode():
292        if module.check_mode:
293            module.exit_json(changed=True)
294
295    if state == 'reloaded':
296        exit_if_check_mode()
297        monit.reload()
298
299    present = monit.is_process_present()
300
301    if not present and not state == 'present':
302        module.fail_json(msg='%s process not presently configured with monit' % name, name=name)
303
304    if state == 'present':
305        if present:
306            module.exit_json(changed=False, name=name, state=state)
307        exit_if_check_mode()
308        monit.present()
309
310    monit.wait_for_monit_to_stop_pending()
311    running = monit.is_process_running()
312
313    if running and state in ['started', 'monitored']:
314        module.exit_json(changed=False, name=name, state=state)
315
316    if running and state == 'stopped':
317        exit_if_check_mode()
318        monit.stop()
319
320    if running and state == 'unmonitored':
321        exit_if_check_mode()
322        monit.unmonitor()
323
324    elif state == 'restarted':
325        exit_if_check_mode()
326        monit.restart()
327
328    elif not running and state == 'started':
329        exit_if_check_mode()
330        monit.start()
331
332    elif not running and state == 'monitored':
333        exit_if_check_mode()
334        monit.monitor()
335
336    module.exit_json(changed=False, name=name, state=state)
337
338
339if __name__ == '__main__':
340    main()
341