refactor and test

This commit is contained in:
Simon Kelly 2020-10-14 16:52:22 +02:00
parent 159f38f4f2
commit 89dbb918a0
2 changed files with 229 additions and 116 deletions

View file

@ -46,6 +46,139 @@ import re
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
STATE_COMMAND_MAP = {
'stopped': 'stop',
'started': 'start',
'monitored': 'monitor',
'unmonitored': 'unmonitor',
'restarted': 'restart'
}
class Monit(object):
def __init__(self, module, monit_bin_path, service_name, timeout):
self.module = module
self.monit_bin_path = monit_bin_path
self.process_name = service_name
self.timeout = timeout
self._monit_version = None
self._sleep_time = 5
def monit_version(self):
if self._monit_version is None:
rc, out, err = self.module.run_command('%s -V' % self.monit_bin_path, check_rc=True)
version_line = out.split('\n')[0]
version = re.search(r"[0-9]+\.[0-9]+", version_line).group().split('.')
# Use only major and minor even if there are more these should be enough
self._monit_version = int(version[0]), int(version[1])
return self._monit_version
def is_version_higher_than_5_18(self):
return self.monit_version() > (5, 18)
@property
def summary_command(self):
return 'summary -B' if self.is_version_higher_than_5_18() else 'summary'
def parse(self, parts):
if self.is_version_higher_than_5_18():
return self.parse_current(parts)
else:
return self.parse_older_versions(parts)
def parse_older_versions(self, parts):
if len(parts) > 2 and parts[0].lower() == 'process' and parts[1] == "'%s'" % self.process_name:
return ' '.join(parts[2:]).lower()
else:
return ''
def parse_current(self, parts):
if len(parts) > 2 and parts[2].lower() == 'process' and parts[0] == self.process_name:
return ''.join(parts[1]).lower()
else:
return ''
def get_status(self):
"""Return the status of the process in monit, or the empty string if not present."""
rc, out, err = self.module.run_command('%s %s' % (self.monit_bin_path, self.summary_command), check_rc=True)
for line in out.split('\n'):
# Sample output lines:
# Process 'name' Running
# Process 'name' Running - restart pending
parts = self.parse(line.split())
if parts != '':
return parts
return ''
def is_process_present(self):
return self.get_status() != ''
def is_process_running(self):
return 'running' in self.get_status()
def run_command(self, command):
"""Runs a monit command, and returns the new status."""
return self.module.run_command('%s %s %s' % (self.monit_bin_path, command, self.process_name), check_rc=True)
def wait_for_monit_to_stop_pending(self, state):
"""Fails this run if there is no status or it's pending/initializing for timeout"""
timeout_time = time.time() + self.timeout
running_status = self.get_status()
while running_status == '' or 'pending' in running_status or 'initializing' in running_status:
if time.time() >= timeout_time:
self.module.fail_json(
msg='waited too long for "pending", or "initiating" status to go away ({0})'.format(
running_status
),
state=state
)
if self._sleep_time:
time.sleep(self._sleep_time)
running_status = self.get_status()
def reload(self):
rc, out, err = self.module.run_command('%s reload' % self.monit_bin_path)
if rc != 0:
self.module.fail_json(msg='monit reload failed', stdout=out, stderr=err)
self.wait_for_monit_to_stop_pending('reloaded')
self.module.exit_json(changed=True, name=self.process_name, state='reloaded')
def present(self):
self.run_command('reload')
if not self.is_process_present():
self.wait_for_monit_to_stop_pending('present')
self.module.exit_json(changed=True, name=self.process_name, state='present')
def change_state(self, state, expected_status, status_contains=None, invert_expected=None):
self.run_command(STATE_COMMAND_MAP[state])
status = self.get_status()
status_match = status in expected_status
if invert_expected:
status_match = not status_match
if status_match or (status_contains and status_contains in status):
self.module.exit_json(changed=True, name=self.process_name, state=state)
self.module.fail_json(msg='%s process not %s' % (self.process_name, state), status=status)
def stop(self):
self.change_state('stopped', ['not monitored'], 'stop pending')
def unmonitor(self):
self.change_state('unmonitored', ['not monitored'], 'unmonitor pending')
def restart(self):
self.change_state('restarted', ['initializing', 'running'], 'restart pending')
def start(self):
self.change_state('started', ['initializing', 'running'], 'start pending')
def monitor(self):
self.change_state('monitored', ['not monitored'], invert_expected=True)
def main(): def main():
arg_spec = dict( arg_spec = dict(
name=dict(required=True), name=dict(required=True),
@ -59,145 +192,52 @@ def main():
state = module.params['state'] state = module.params['state']
timeout = module.params['timeout'] timeout = module.params['timeout']
MONIT = module.get_bin_path('monit', True) monit = Monit(module, module.get_bin_path('monit', True), name, timeout)
def monit_version(): def exit_if_check_mode():
rc, out, err = module.run_command('%s -V' % MONIT, check_rc=True)
version_line = out.split('\n')[0]
version = re.search(r"[0-9]+\.[0-9]+", version_line).group().split('.')
# Use only major and minor even if there are more these should be enough
return int(version[0]), int(version[1])
def is_version_higher_than_5_18():
return (MONIT_MAJOR_VERSION, MONIT_MINOR_VERSION) > (5, 18)
def parse(parts):
if is_version_higher_than_5_18():
return parse_current(parts)
else:
return parse_older_versions(parts)
def parse_older_versions(parts):
if len(parts) > 2 and parts[0].lower() == 'process' and parts[1] == "'%s'" % name:
return ' '.join(parts[2:]).lower()
else:
return ''
def parse_current(parts):
if len(parts) > 2 and parts[2].lower() == 'process' and parts[0] == name:
return ''.join(parts[1]).lower()
else:
return ''
def get_status():
"""Return the status of the process in monit, or the empty string if not present."""
rc, out, err = module.run_command('%s %s' % (MONIT, SUMMARY_COMMAND), check_rc=True)
for line in out.split('\n'):
# Sample output lines:
# Process 'name' Running
# Process 'name' Running - restart pending
parts = parse(line.split())
if parts != '':
return parts
return ''
def run_command(command):
"""Runs a monit command, and returns the new status."""
module.run_command('%s %s %s' % (MONIT, command, name), check_rc=True)
return get_status()
def wait_for_monit_to_stop_pending():
"""Fails this run if there is no status or it's pending/initializing for timeout"""
timeout_time = time.time() + timeout
sleep_time = 5
running_status = get_status()
while running_status == '' or 'pending' in running_status or 'initializing' in running_status:
if time.time() >= timeout_time:
module.fail_json(
msg='waited too long for "pending", or "initiating" status to go away ({0})'.format(
running_status
),
state=state
)
time.sleep(sleep_time)
running_status = get_status()
MONIT_MAJOR_VERSION, MONIT_MINOR_VERSION = monit_version()
SUMMARY_COMMAND = ('summary', 'summary -B')[is_version_higher_than_5_18()]
if state == 'reloaded':
if module.check_mode: if module.check_mode:
module.exit_json(changed=True) module.exit_json(changed=True)
rc, out, err = module.run_command('%s reload' % MONIT)
if rc != 0:
module.fail_json(msg='monit reload failed', stdout=out, stderr=err)
wait_for_monit_to_stop_pending()
module.exit_json(changed=True, name=name, state=state)
present = get_status() != '' if state == 'reloaded':
exit_if_check_mode()
monit.reload()
present = monit.is_process_present()
if not present and not state == 'present': if not present and not state == 'present':
module.fail_json(msg='%s process not presently configured with monit' % name, name=name, state=state) module.fail_json(msg='%s process not presently configured with monit' % name, name=name, state=state)
if state == 'present': if state == 'present':
if not present: if present:
if module.check_mode: module.exit_json(changed=False, name=name, state=state)
module.exit_json(changed=True) exit_if_check_mode()
status = run_command('reload') monit.present()
if status == '':
wait_for_monit_to_stop_pending()
module.exit_json(changed=True, name=name, state=state)
module.exit_json(changed=False, name=name, state=state)
wait_for_monit_to_stop_pending() monit.wait_for_monit_to_stop_pending(state)
running = 'running' in get_status() running = monit.is_process_running()
if running and state in ['started', 'monitored']: if running and state in ['started', 'monitored']:
module.exit_json(changed=False, name=name, state=state) module.exit_json(changed=False, name=name, state=state)
if running and state == 'stopped': if running and state == 'stopped':
if module.check_mode: exit_if_check_mode()
module.exit_json(changed=True) monit.stop()
status = run_command('stop')
if status in ['not monitored'] or 'stop pending' in status:
module.exit_json(changed=True, name=name, state=state)
module.fail_json(msg='%s process not stopped' % name, status=status)
if running and state == 'unmonitored': if running and state == 'unmonitored':
if module.check_mode: exit_if_check_mode()
module.exit_json(changed=True) monit.unmonitor()
status = run_command('unmonitor')
if status in ['not monitored'] or 'unmonitor pending' in status:
module.exit_json(changed=True, name=name, state=state)
module.fail_json(msg='%s process not unmonitored' % name, status=status)
elif state == 'restarted': elif state == 'restarted':
if module.check_mode: exit_if_check_mode()
module.exit_json(changed=True) monit.restart()
status = run_command('restart')
if status in ['initializing', 'running'] or 'restart pending' in status:
module.exit_json(changed=True, name=name, state=state)
module.fail_json(msg='%s process not restarted' % name, status=status)
elif not running and state == 'started': elif not running and state == 'started':
if module.check_mode: exit_if_check_mode()
module.exit_json(changed=True) monit.start()
status = run_command('start')
if status in ['initializing', 'running'] or 'start pending' in status:
module.exit_json(changed=True, name=name, state=state)
module.fail_json(msg='%s process not started' % name, status=status)
elif not running and state == 'monitored': elif not running and state == 'monitored':
if module.check_mode: exit_if_check_mode()
module.exit_json(changed=True) monit.monitor()
status = run_command('monitor')
if status not in ['not monitored']:
module.exit_json(changed=True, name=name, state=state)
module.fail_json(msg='%s process not monitored' % name, status=status)
module.exit_json(changed=False, name=name, state=state) module.exit_json(changed=False, name=name, state=state)

View file

@ -0,0 +1,73 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from unittest import mock
from unittest.mock import MagicMock
from ansible_collections.community.general.tests.unit.compat import unittest
from ansible_collections.community.general.plugins.modules.monitoring import monit
class AnsibleExitJson(Exception):
"""Exception class to be raised by module.exit_json and caught by the test case"""
pass
class AnsibleFailJson(Exception):
"""Exception class to be raised by module.fail_json and caught by the test case"""
pass
class MonitTest(unittest.TestCase):
def setUp(self):
self.module = MagicMock()
self.module.exit_json.side_effect = AnsibleExitJson(Exception)
self.monit = monit.Monit(self.module, 'monit', 'processX', 1)
self.version_patch = mock.patch.object(self.monit, "is_version_higher_than_5_18", return_value=True)
self.version_patch.start()
def tearDown(self):
self.version_patch.stop()
def patch_status(self, side_effect):
return mock.patch.object(self.monit, 'get_status', side_effect=side_effect)
def test_change_state_success(self):
with self.patch_status(['not monitored']), 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(['monitored']):
self.monit.stop()
self.module.fail_json.assert_called()
def test_reload_fail(self):
self.module.run_command.return_value = (1, 'stdout', 'stderr')
self.module.fail_json.side_effect = AnsibleFailJson(Exception)
with self.assertRaises(AnsibleFailJson):
self.monit.reload()
def test_reload(self):
self.module.run_command.return_value = (0, '', '')
self.monit._sleep_time = 0
with self.patch_status(['', 'pending', 'running']), self.assertRaises(AnsibleExitJson):
self.monit.reload()
def test_monitor(self):
with self.patch_status(['not monitored - start pending']), self.assertRaises(AnsibleExitJson):
self.monit.monitor()
def test_monitor_fail(self):
with self.patch_status(['not monitored']):
self.monit.monitor()
self.module.fail_json.assert_called()
def test_timeout(self):
self.monit.timeout = 0
self.module.fail_json.side_effect = AnsibleFailJson(Exception)
with self.patch_status(['stop pending']), self.assertRaises(AnsibleFailJson):
self.monit.wait_for_monit_to_stop_pending('stopped')