mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-09 12:10:31 -07:00
add integration tests + fixes
This commit is contained in:
parent
00152dbb63
commit
816f31c19f
13 changed files with 393 additions and 25 deletions
|
@ -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)
|
||||
|
|
1
tests/integration/targets/monit/aliases
Normal file
1
tests/integration/targets/monit/aliases
Normal file
|
@ -0,0 +1 @@
|
|||
destructive
|
124
tests/integration/targets/monit/files/daemon.py
Normal file
124
tests/integration/targets/monit/files/daemon.py
Normal 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()."""
|
57
tests/integration/targets/monit/files/httpd_echo.py
Normal file
57
tests/integration/targets/monit/files/httpd_echo.py
Normal 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)
|
16
tests/integration/targets/monit/handlers/main.yml
Normal file
16
tests/integration/targets/monit/handlers/main.yml
Normal 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
|
2
tests/integration/targets/monit/meta/main.yml
Normal file
2
tests/integration/targets/monit/meta/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
dependencies:
|
||||
- setup_pkg_mgr
|
53
tests/integration/targets/monit/tasks/main.yml
Normal file
53
tests/integration/targets/monit/tasks/main.yml
Normal 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
|
26
tests/integration/targets/monit/tasks/test.yml
Normal file
26
tests/integration/targets/monit/tasks/test.yml
Normal 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
|
6
tests/integration/targets/monit/tasks/test_errors.yml
Normal file
6
tests/integration/targets/monit/tasks/test_errors.yml
Normal 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)
|
51
tests/integration/targets/monit/tasks/test_state.yml
Normal file
51
tests/integration/targets/monit/tasks/test_state.yml
Normal 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
|
4
tests/integration/targets/monit/templates/httpd_echo.j2
Normal file
4
tests/integration/targets/monit/templates/httpd_echo.j2
Normal 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
|
14
tests/integration/targets/monit/templates/monitrc.j2
Normal file
14
tests/integration/targets/monit/templates/monitrc.j2
Normal 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/*
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue