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