add integration tests + fixes

This commit is contained in:
Simon Kelly 2020-10-16 15:04:06 +02:00
parent 00152dbb63
commit 816f31c19f
13 changed files with 393 additions and 25 deletions

View file

@ -61,22 +61,22 @@ ALL_STATUS = [
]
class StatusValue(namedtuple("Status", "status, is_pending")):
class StatusValue(namedtuple("Status", "value, is_pending")):
MISSING = 0
OK = 1
NOT_MONITORED = 2
INITIALIZING = 3
DOES_NOT_EXIST = 4
def __new__(cls, status, is_pending=False):
return super(StatusValue, cls).__new__(cls, status, is_pending)
def __new__(cls, value, is_pending=False):
return super(StatusValue, cls).__new__(cls, value, is_pending)
def pending(self):
return StatusValue(self.status, True)
return StatusValue(self.value, True)
def __getattr__(self, item):
if item in ('is_%s' % status for status in ALL_STATUS):
return self.status == getattr(self, item[3:].upper())
return self.value == getattr(self, item[3:].upper())
class Status(object):
@ -122,22 +122,24 @@ class Monit(object):
return Status.MISSING
status_val = re.findall(r"^\s*status\s*([\w\- ]+)", output, re.MULTILINE)
if status_val:
status_val = status_val[0].strip().upper()
if ' - ' not in status_val:
status_val.replace(' ', '_')
return getattr(Status, status_val)
else:
status_val, substatus = status_val.split(' - ')
action, state = substatus.split()
if action in ['START', 'INITIALIZING', 'RESTART', 'MONITOR']:
status = Status.OK
else:
status = Status.NOT_MONITORED
if not status_val:
self.module.fail_json(msg="Unable to find process status")
if state == 'pending':
status = status.pending()
return status
status_val = status_val[0].strip().upper()
if ' - ' not in status_val:
status_val = status_val.replace(' ', '_')
return getattr(Status, status_val)
else:
status_val, substatus = status_val.split(' - ')
action, state = substatus.split()
if action in ['START', 'INITIALIZING', 'RESTART', 'MONITOR']:
status = Status.OK
else:
status = Status.NOT_MONITORED
if state == 'pending':
status = status.pending()
return status
def is_process_present(self):
rc, out, err = self.module.run_command('%s summary -B' % (self.monit_bin_path), check_rc=True)
@ -155,7 +157,12 @@ class Monit(object):
timeout_time = time.time() + self.timeout
running_status = self.get_status()
while running_status.is_missing or running_status.is_pending or running_status.is_initializing:
waiting_status = [
StatusValue.MISSING,
StatusValue.INITIALIZING,
StatusValue.DOES_NOT_EXIST,
]
while running_status.is_pending or (running_status.value in waiting_status):
if time.time() >= timeout_time:
self.module.fail_json(
msg='waited too long for "pending", or "initiating" status to go away ({0})'.format(
@ -184,12 +191,12 @@ class Monit(object):
def change_state(self, state, expected_status, invert_expected=None):
self.run_command(STATE_COMMAND_MAP[state])
status = self.get_status()
status_match = status.status == expected_status.status
status_match = status.value == expected_status.value
if invert_expected:
status_match = not status_match
if status_match:
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)
self.module.fail_json(msg='%s process not %s' % (self.process_name, state), status=repr(status))
def stop(self):
self.change_state('stopped', Status.NOT_MONITORED)

View file

@ -0,0 +1 @@
destructive

View file

@ -0,0 +1,124 @@
"""Generic linux daemon base class for python.
http://www.jejik.com/files/examples/daemon3x.py
"""
import sys, os, time, atexit, signal
class Daemon:
"""A generic daemon class.
Usage: subclass the daemon class and override the run() method."""
def __init__(self, pidfile):
self.pidfile = pidfile
def daemonize(self):
"""Deamonize class. UNIX double fork mechanism."""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile, 'w+') as f:
f.write(pid + '\n')
def delpid(self):
os.remove(self.pidfile)
def start(self):
"""Start the daemon."""
# Check for a pidfile to see if the daemon already runs
try:
with open(self.pidfile, 'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if pid:
message = "pidfile {0} already exist. " + \
"Daemon already running?\n"
sys.stderr.write(message.format(self.pidfile))
sys.exit(1)
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""Stop the daemon."""
# Get the pid from the pidfile
try:
with open(self.pidfile, 'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if not pid:
message = "pidfile {0} does not exist. " + \
"Daemon not running?\n"
sys.stderr.write(message.format(self.pidfile))
return # not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
e = str(err.args)
if e.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print(str(err.args))
sys.exit(1)
def restart(self):
"""Restart the daemon."""
self.stop()
self.start()
def run(self):
"""You should override this method when you subclass Daemon.
It will be called after the process has been daemonized by
start() or restart()."""

View file

@ -0,0 +1,57 @@
import sys
from daemon import Daemon
try:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
def write_to_output(stream, content):
stream.write(content)
except ImportError:
from http.server import BaseHTTPRequestHandler, HTTPServer
def write_to_output(stream, content):
stream.write(bytes(content, "utf-8"))
hostname = "localhost"
server_port = 8082
class MyServer(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
write_to_output(self.wfile, self.path)
class MyDaemon(Daemon):
def run(self):
webServer = HTTPServer((hostname, server_port), MyServer)
print("Server started http://%s:%s" % (hostname, server_port))
try:
webServer.serve_forever()
except KeyboardInterrupt:
pass
webServer.server_close()
print("Server stopped.")
if __name__ == "__main__":
daemon = MyDaemon('/tmp/httpd_echo.pid')
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
daemon.start()
elif 'stop' == sys.argv[1]:
daemon.stop()
elif 'restart' == sys.argv[1]:
daemon.restart()
else:
print("Unknown command")
sys.exit(2)
sys.exit(0)
else:
print("usage: %s start|stop|restart" % sys.argv[0])
sys.exit(2)

View file

@ -0,0 +1,16 @@
---
- name: start monit
become: yes
service: name=monit state=started
- name: restart monit
become: yes
service: name=monit state=restarted
- name: reload monit
become: yes
service: name=monit state=reloaded
- name: stop monit
become: yes
service: name=monit state=stopped

View file

@ -0,0 +1,2 @@
dependencies:
- setup_pkg_mgr

View file

@ -0,0 +1,53 @@
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
- block:
- name: install monit
become: yes
package:
name: monit
state: present
- name: monit config
become: yes
template:
src: "monitrc.j2"
dest: "/etc/monit/monitrc"
- name: process monit config
become: yes
template:
src: "httpd_echo.j2"
dest: "/etc/monit/conf.d/httpd_echo"
- name: copy process file
become: yes
copy:
src: "{{item}}"
dest: "/opt/{{item}}"
loop:
- daemon.py
- httpd_echo.py
- name: restart monit
become: yes
service:
name: monit
state: restarted
- include_tasks: test.yml
always:
- name: stop monit
become: yes
service:
name: monit
state: stopped
- name: uninstall monit
become: yes
package:
name: monit
state: absent

View file

@ -0,0 +1,26 @@
# order is important
- import_tasks: test_state.yml
vars:
state: stopped
initial_state: up
expected_state: down
- import_tasks: test_state.yml
vars:
state: started
initial_state: down
expected_state: up
- import_tasks: test_state.yml
vars:
state: unmonitored
initial_state: up
expected_state: down
- import_tasks: test_state.yml
vars:
state: monitored
initial_state: down
expected_state: up
- import_tasks: test_errors.yml

View file

@ -0,0 +1,6 @@
- name: Check an error occurs when wrong process name is used
monit:
name: missing
state: started
register: result
failed_when: result is not skip and (result is success or result is not failed)

View file

@ -0,0 +1,51 @@
- name: verify initial state (up)
command: "curl -sf http://localhost:8082/hello"
args:
warn: false
when: initial_state == 'up'
- name: verify initial state (down)
command: "curl -sf http://localhost:8082/hello"
args:
warn: false
register: curl_result
failed_when: curl_result == 0
when: initial_state == 'down'
- name: change httpd_echo process state to {{ state }}
monit:
name: httpd_echo
state: "{{ state }}"
register: result
- name: check that state changed
assert:
that:
- result is success
- result is changed
- name: check that service is {{ state }} (expected 'up')
command: "curl -sf http://localhost:8082/hello"
args:
warn: false
when: expected_state == 'up'
- name: check that service is {{ state }} (expected 'down')
command: "curl -sf http://localhost:8082/hello"
args:
warn: false
register: curl_result
failed_when: curl_result == 0
when: expected_state == 'down'
- name: try change state again to {{ state }}
monit:
name: httpd_echo
state: "{{ state }}"
register: result
- name: check that state is not changed
assert:
that:
- result is success
- result is not changed

View file

@ -0,0 +1,4 @@
check process httpd_echo with pidfile /tmp/httpd_echo.pid
start program = "{{ansible_python.executable}} /opt/httpd_echo.py start"
stop program = "{{ansible_python.executable}} /opt/httpd_echo.py stop"
if failed host localhost port 8082 then restart

View file

@ -0,0 +1,14 @@
set daemon 2
set logfile /var/log/monit.log
set idfile /var/lib/monit/id
set statefile /var/lib/monit/state
set eventqueue
basedir /var/lib/monit/events
slots 100
set httpd port 2812 and
use address localhost
allow localhost
include /etc/monit/conf.d/*

View file

@ -55,16 +55,21 @@ class MonitTest(unittest.TestCase):
def test_reload(self):
self.module.run_command.return_value = (0, '', '')
with self.patch_status(monit.Status.OK) as get_status:
with self.assertRaises(AnsibleExitJson):
self.monit.reload()
def test_wait_for_status(self):
self.monit._sleep_time = 0
status = [
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:
with self.assertRaises(AnsibleExitJson):
self.monit.reload()
self.monit.wait_for_monit_to_stop_pending('ok')
self.assertEqual(get_status.call_count, len(status))
def test_monitor(self):
@ -105,6 +110,8 @@ BASIC_OUTPUT_CASES = [
(TEST_OUTPUT % ('processX', 'Monitored - stop pending'), monit.Status.NOT_MONITORED),
(TEST_OUTPUT % ('processX', 'Monitored - restart pending'), monit.Status.OK),
(TEST_OUTPUT % ('processX', 'Not Monitored - monitor pending'), monit.Status.OK),
(TEST_OUTPUT % ('processX', 'Does not exist'), monit.Status.DOES_NOT_EXIST),
(TEST_OUTPUT % ('processX', 'Not monitored'), monit.Status.NOT_MONITORED),
])
def test_parse_status(output, expected):
status = monit.Monit(None, '', 'processX', 0)._parse_status(output)