nagios-charmers team mailing list archive
-
nagios-charmers team
-
Mailing list archive
-
Message #01026
[Merge] ~addyess/charm-nagios:modern_testing into charm-nagios:master
Adam Dyess has proposed merging ~addyess/charm-nagios:modern_testing into charm-nagios:master with ~addyess/charm-nagios:lp1849575-fix_ssl_only_config as a prerequisite.
Commit message:
Adding unit test and functional tests framework
Requested reviews:
Nagios Charm developers (nagios-charmers)
For more details, see:
https://code.launchpad.net/~addyess/charm-nagios/+git/charm-nagios/+merge/387315
--
Your team Nagios Charm developers is requested to review the proposed merge of ~addyess/charm-nagios:modern_testing into charm-nagios:master.
diff --git a/.gitignore b/.gitignore
index 0ab81b3..ceabe0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,7 @@ data/*.key
data/*.crt
data/*.csr
.tox/
-.idea
+.idea/
+reports/
+.coverage
repo-info
diff --git a/Makefile b/Makefile
index dbbeab3..7ad8e68 100644
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,8 @@ endif
default:
echo Nothing to do
+test: lint unittest functional
+
lint:
@echo "Running flake8"
@tox -e lint
@@ -22,16 +24,15 @@ build:
@cp -r * $(CHARM_BUILD_DIR)/$(CHARM_NAME)
# Primitive test runner. Someone please fix this.
-test:
- tests/00-setup
- tests/100-deploy
- tests/10-initial-test
- tests/15-nrpe-test
- tests/20-ssl-test
- tests/21-monitors-interface-test
- tests/22-extraconfig-test
- tests/23-livestatus-test
- tests/24-pagerduty-test
+functional: build
+ @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \
+ PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \
+ PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \
+ tox -e functional
+
+unittest:
+ @tox -e unit
+
bin/charm_helpers_sync.py:
@mkdir -p bin
diff --git a/hooks/common.py b/hooks/common.py
index 66d41ec..df54111 100644
--- a/hooks/common.py
+++ b/hooks/common.py
@@ -92,7 +92,7 @@ def get_ip_and_hostname(remote_unit, relation_id=None):
return (ip_address, remote_unit.replace('/', '-'))
-def refresh_hostgroups():
+def refresh_hostgroups(): # noqa:C901
""" Not the most efficient thing but since we're only
parsing what is already on disk here its not too bad """
hosts = [x['host_name'] for x in Model.Host.objects.all if x['host_name']]
diff --git a/hooks/monitors-relation-broken b/hooks/monitors-relation-broken
index 880e0af..47150e0 120000
--- a/hooks/monitors-relation-broken
+++ b/hooks/monitors-relation-broken
@@ -1 +1 @@
-monitors-relation-changed
\ No newline at end of file
+monitors_relation_changed.py
\ No newline at end of file
diff --git a/hooks/monitors-relation-changed b/hooks/monitors-relation-changed
new file mode 120000
index 0000000..47150e0
--- /dev/null
+++ b/hooks/monitors-relation-changed
@@ -0,0 +1 @@
+monitors_relation_changed.py
\ No newline at end of file
diff --git a/hooks/monitors-relation-departed b/hooks/monitors-relation-departed
index 880e0af..47150e0 120000
--- a/hooks/monitors-relation-departed
+++ b/hooks/monitors-relation-departed
@@ -1 +1 @@
-monitors-relation-changed
\ No newline at end of file
+monitors_relation_changed.py
\ No newline at end of file
diff --git a/hooks/monitors-relation-changed b/hooks/monitors_relation_changed.py
similarity index 86%
rename from hooks/monitors-relation-changed
rename to hooks/monitors_relation_changed.py
index 13cb96c..bd37812 100755
--- a/hooks/monitors-relation-changed
+++ b/hooks/monitors_relation_changed.py
@@ -23,15 +23,14 @@ import yaml
import json
import re
-
from common import (customize_service, get_pynag_host,
- get_pynag_service, refresh_hostgroups,
- get_valid_relations, get_valid_units,
- initialize_inprogress_config, flush_inprogress_config,
- get_local_ingress_address)
+ get_pynag_service, refresh_hostgroups,
+ get_valid_relations, get_valid_units,
+ initialize_inprogress_config, flush_inprogress_config,
+ get_local_ingress_address)
-def main(argv):
+def main(argv): # noqa: C901
# Note that one can pass in args positionally, 'monitors.yaml targetid
# and target-address' so the hook can be tested without being in a hook
# context.
@@ -48,9 +47,9 @@ def main(argv):
(relname, relnum) = relid.split(':')
for unit in get_valid_units(relid):
relation_settings = json.loads(
- subprocess.check_output(['relation-get', '--format=json',
- '-r', relid,
- '-',unit]).strip())
+ subprocess.check_output(['relation-get', '--format=json',
+ '-r', relid,
+ '-', unit]).strip())
if relation_settings is None or relation_settings == '':
continue
@@ -59,14 +58,20 @@ def main(argv):
if ('monitors' not in relation_settings
or 'target-id' not in relation_settings):
continue
- if ('target-id' in relation_settings and 'target-address' not in relation_settings):
- relation_settings['target-address'] = get_local_ingress_address('monitors')
+ if (
+ 'target-id' in relation_settings and
+ 'target-address' not in relation_settings):
+ relation_settings[
+ 'target-address'] = get_local_ingress_address(
+ 'monitors')
else:
# Fake it for the more generic 'nagios' relation'
- relation_settings['target-id'] = unit.replace('/','-')
- relation_settings['target-address'] = get_local_ingress_address('monitors')
- relation_settings['monitors'] = {'monitors': {'remote': {} } }
+ relation_settings['target-id'] = unit.replace('/', '-')
+ relation_settings[
+ 'target-address'] = get_local_ingress_address(
+ 'monitors')
+ relation_settings['monitors'] = {'monitors': {'remote': {}}}
if relid not in all_relations:
all_relations[relid] = {}
@@ -103,21 +108,22 @@ def main(argv):
os.system('service nagios3 reload')
-def apply_relation_config(relid, units, all_hosts):
+def apply_relation_config(relid, units, all_hosts): # noqa: C901
for unit, relation_settings in units.iteritems():
monitors = relation_settings['monitors']
target_id = relation_settings['target-id']
machine_id = relation_settings.get('machine_id', None)
parent_host = None
if machine_id:
- container_regex = re.compile("(\d*)/lx[cd]/\d*")
+ container_regex = re.compile(r"(\d+)/lx[cd]/\d+")
if container_regex.search(machine_id):
parent_machine = container_regex.search(machine_id).group(1)
if parent_machine in all_hosts:
parent_host = all_hosts[parent_machine]
# If not set, we don't mess with it, as multiple services may feed
- # monitors in for a particular address. Generally a primary will set this
+ # monitors in for a particular address. Generally a primary will set
+ # this
# to its own private-address
target_address = relation_settings.get('target-address', None)
diff --git a/hooks/test-common.py b/hooks/test-common.py
deleted file mode 100644
index b7be677..0000000
--- a/hooks/test-common.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from common import ObjectTagCollection
-import os
-
-from tempfile import NamedTemporaryFile
-
-""" This is meant to test the ObjectTagCollection bits. It should
- probably be made into a proper unit test. """
-
-x = ObjectTagCollection('test-units')
-y = ObjectTagCollection('test-relids')
-
-o = NamedTemporaryFile(delete=False)
-o2 = NamedTemporaryFile(delete=False)
-o3 = NamedTemporaryFile(delete=True)
-o.write('some content')
-o.flush()
-
-x.tag_object(o.name, 'box-9')
-x.tag_object(o.name, 'nrpe-1')
-y.tag_object(o.name, 'monitors:2')
-x.tag_object(o2.name, 'box-10')
-x.tag_object(o2.name, 'nrpe-2')
-y.tag_object(o2.name, 'monitors:2')
-x.tag_object(o3.name, 'other-0')
-y.tag_object(o3.name, 'monitors:3')
-x.untag_object(o.name, 'box-9')
-x.cleanup_untagged()
-
-if not os.path.exists(o.name):
- raise RuntimeError(o.name)
-
-x.kill_tag('nrpe-1')
-x.cleanup_untagged()
-
-if os.path.exists(o.name):
- raise RuntimeError(o.name)
-
-if not os.path.exists(o2.name):
- raise RuntimeError(o2.name)
-
-y.kill_tag('monitors:2')
-y.cleanup_untagged(['monitors:1', 'monitors:3'])
-
-if os.path.exists(o.name):
- raise RuntimeError(o2.name)
-
-if os.path.exists(o2.name):
- raise RuntimeError(o2.name)
-
-x.destroy()
diff --git a/hooks/upgrade-charm b/hooks/upgrade-charm
new file mode 120000
index 0000000..d378ed9
--- /dev/null
+++ b/hooks/upgrade-charm
@@ -0,0 +1 @@
+upgrade_charm.py
\ No newline at end of file
diff --git a/hooks/upgrade-charm b/hooks/upgrade_charm.py
similarity index 100%
rename from hooks/upgrade-charm
rename to hooks/upgrade_charm.py
diff --git a/hooks/website-relation-joined b/hooks/website-relation-joined
new file mode 120000
index 0000000..e289c51
--- /dev/null
+++ b/hooks/website-relation-joined
@@ -0,0 +1 @@
+website_relation_joined.py
\ No newline at end of file
diff --git a/hooks/website-relation-joined b/hooks/website_relation_joined.py
similarity index 96%
rename from hooks/website-relation-joined
rename to hooks/website_relation_joined.py
index dc3fec0..984ae80 100755
--- a/hooks/website-relation-joined
+++ b/hooks/website_relation_joined.py
@@ -23,6 +23,7 @@ from charmhelpers.core.hookenv import (
relation_set,
)
+
def main():
relation_data = {'hostname': common.get_local_ingress_address()}
sslcfg = config()['ssl']
@@ -34,5 +35,5 @@ def main():
relation_set(None, **relation_data)
-if __name__ == '__main__':
+if __name__ == '__main__': # pragma: no cover
main()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..5c6abac
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+pynag
+jinja2
\ No newline at end of file
diff --git a/tests/00-setup b/tests/00-setup
deleted file mode 100755
index f46efde..0000000
--- a/tests/00-setup
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-
-sudo add-apt-repository -y ppa:juju/stable
-sudo apt-get update
-sudo apt-get install -y amulet juju-local python3-requests
diff --git a/tests/10-initial-test b/tests/10-initial-test
deleted file mode 100755
index bbf431b..0000000
--- a/tests/10-initial-test
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/usr/bin/python3
-
-import amulet
-import requests
-
-seconds = 20000
-
-d = amulet.Deployment(series='trusty')
-
-d.add('nagios')
-d.add('mysql')
-d.add('mediawiki')
-
-d.relate('mysql:db', 'mediawiki:db')
-d.relate('nagios:monitors', 'mysql:monitors')
-d.relate('nagios:nagios', 'mediawiki:juju-info')
-
-d.expose('nagios')
-
-try:
- d.setup(timeout=seconds)
-except amulet.helpers.TimeoutError:
- amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
-except:
- raise
-
-
-##
-# Set relationship aliases
-##
-mysql_unit = d.sentry['mysql'][0]
-mediawiki_unit = d.sentry['mediawiki'][0]
-nagios_unit = d.sentry['nagios'][0]
-
-
-# Validate that the web interface has htpasswd authentication
-def test_web_interface_is_protected():
- r = requests.get("http://%s/nagios3/" % nagios_unit.info['public-address'])
- if r.status_code != 401:
- amulet.raise_status(amulet.FAIL, msg="Web Interface open to the world")
- # validate that our configured admin is valid
- nagpwd = nagios_unit.file_contents('/var/lib/juju/nagios.passwd').strip()
- r = requests.get("http://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd))
- if r.status_code != 200:
- amulet.raise_status(amulet.FAIL, msg="Web Admin login failed")
-
-
-def test_hosts_being_monitored():
- nagpwd = nagios_unit.file_contents('/var/lib/juju/nagios.passwd').strip()
- host_url = ("http://%s/cgi-bin/nagios3/status.cgi?"
- "hostgroup=all&style=hostdetail")
- r = requests.get(host_url % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd))
- if not (r.text.find('mysql') and r.text.find('mediawiki')):
- amulet.raise_status(amulet.ERROR,
- msg='Nagios is not monitoring the' +
- ' hosts it supposed to.')
-
-
-test_web_interface_is_protected()
-test_hosts_being_monitored()
diff --git a/tests/15-nrpe-test b/tests/15-nrpe-test
deleted file mode 100755
index 097cff4..0000000
--- a/tests/15-nrpe-test
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/python3
-
-import amulet
-import requests
-
-seconds = 20000
-
-d = amulet.Deployment(series='trusty')
-
-d.add('nagios')
-d.add('mysql')
-d.add('mediawiki')
-d.add('nrpe')
-
-d.relate('mysql:db', 'mediawiki:db')
-d.relate('nagios:monitors', 'mysql:monitors')
-d.relate('nagios:nagios', 'mediawiki:juju-info')
-d.relate('nrpe:general-info', 'mediawiki:juju-info')
-d.relate('nrpe:general-info', 'mysql:juju-info')
-d.relate('nrpe:monitors', 'nagios:monitors')
-
-d.expose('nagios')
-
-try:
- d.setup(timeout=seconds)
- d.sentry.wait()
-except amulet.helpers.TimeoutError:
- amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
-except:
- raise
-
-
-##
-# Set relationship aliases
-##
-mysql_unit = d.sentry['mysql'][0]
-mediawiki_unit = d.sentry['mediawiki'][0]
-nagios_unit = d.sentry['nagios'][0]
-
-
-# Validate that the web interface has htpasswd authentication
-def test_web_interface_is_protected():
- r = requests.get("http://%s/nagios3/" % nagios_unit.info['public-address'])
- if r.status_code != 401:
- amulet.raise_status(amulet.FAIL, msg="Web Interface open to the world")
- # validate that our configured admin is valid
- nagpwd = nagios_unit.file_contents('/var/lib/juju/nagios.passwd').strip()
- r = requests.get("http://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd))
- if r.status_code != 200:
- amulet.raise_status(amulet.FAIL, msg="Web Admin login failed")
-
-
-def test_hosts_being_monitored():
- nagpwd = nagios_unit.file_contents('/var/lib/juju/nagios.passwd').strip()
- host_url = ("http://%s/cgi-bin/nagios3/status.cgi?"
- "host=all")
- r = requests.get(host_url % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd))
- if not (r.text.find('mysql-0-mem') and r.text.find('mediawiki-0-mem')):
- amulet.raise_status(amulet.ERROR,
- msg='Nagios nrpe is not monitoring the' +
- ' hosts it supposed to.')
-
-
-test_web_interface_is_protected()
-test_hosts_being_monitored()
diff --git a/tests/20-ssl-test b/tests/20-ssl-test
deleted file mode 100755
index 599a44f..0000000
--- a/tests/20-ssl-test
+++ /dev/null
@@ -1,117 +0,0 @@
-#!/usr/bin/python3
-
-import sys
-
-import amulet
-import requests
-
-seconds = 20000
-
-d = amulet.Deployment(series='trusty')
-
-d.add('nagios')
-d.add('mysql')
-d.add('mediawiki')
-
-d.relate('mysql:db', 'mediawiki:db')
-d.relate('nagios:monitors', 'mysql:monitors')
-d.relate('nagios:nagios', 'mediawiki:juju-info')
-
-d.expose('nagios')
-
-try:
- d.setup(timeout=seconds)
- d.sentry.wait()
-except amulet.helpers.TimeoutError:
- amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
-except:
- raise
-
-
-##
-# Set relationship aliases
-##
-mysql_unit = d.sentry['mysql'][0]
-mediawiki_unit = d.sentry['mediawiki'][0]
-nagios_unit = d.sentry['nagios'][0]
-
-
-def test_web_interface_without_ssl():
- d.configure('nagios', {
- 'ssl': 'off'
- })
- d.sentry.wait()
-
- nagpwd = nagios_unit.file_contents('/var/lib/juju/nagios.passwd').strip()
- r = requests.get("http://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd))
- if r.status_code != 200:
- amulet.raise_status(amulet.FAIL,
- msg="Error connecting without ssl, when ssl=off")
-
- try:
- r = requests.get(
- "https://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd), verify=False)
- except requests.ConnectionError:
- pass
- else:
- amulet.raise_status(amulet.FAIL, msg='Accepting SSL when ssl is off.')
-
-
-def test_web_interface_with_ssl():
- d.configure('nagios', {
- 'ssl': 'on'
- })
- d.sentry.wait()
-
- nagpwd = nagios_unit.file_contents('/var/lib/juju/nagios.passwd').strip()
- r = requests.get("http://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd))
- if r.status_code != 200:
- amulet.raise_status(amulet.FAIL,
- msg="Error connecting without ssl, when ssl=on")
-
- try:
- r = requests.get(
- "https://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd), verify=False)
- if r.status_code != 200:
- amulet.raise_status(amulet.FAIL,
- msg="Error connecting with ssl, when ssl=on")
- except requests.ConnectionError:
- amulet.raise_status(amulet.FAIL,
- msg=('Error connecting with ssl, when ssl=on.'
- ' Error %s' % sys.exc_info()[0]))
-
-
-def test_web_interface_with_only_ssl():
- d.configure('nagios', {
- 'ssl': 'only'
- })
- d.sentry.wait()
-
- nagpwd = nagios_unit.file_contents('/var/lib/juju/nagios.passwd').strip()
- r = requests.get(
- "https://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd), verify=False)
- if r.status_code != 200:
- amulet.raise_status(amulet.FAIL,
- msg=("Error connecting with ssl, when ssl=only. "
- "Status Code: %s" % r.status_code))
-
- try:
- r = requests.get(
- "http://%s/nagios3/" % nagios_unit.info['public-address'],
- auth=('nagiosadmin', nagpwd))
- if r.status_code == 200:
- amulet.raise_status(amulet.FAIL,
- msg=("Error connecting without ssl,"
- " when ssl=only."
- "Status Code: %s" % r.status_code))
- except requests.ConnectionError:
- pass
-
-test_web_interface_without_ssl()
-test_web_interface_with_ssl()
-test_web_interface_with_only_ssl()
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
new file mode 100644
index 0000000..aa42a09
--- /dev/null
+++ b/tests/functional/conftest.py
@@ -0,0 +1,169 @@
+#!/usr/bin/python3
+
+import asyncio
+import json
+import os
+import uuid
+
+import juju
+from juju.controller import Controller
+from juju.errors import JujuError
+from juju.model import Model
+
+import pytest
+
+
+STAT_FILE = "python3 -c \"import json; import os; s=os.stat('%s'); print(json.dumps({'uid': s.st_uid, 'gid': s.st_gid, 'mode': oct(s.st_mode), 'size': s.st_size}))\"" # noqa: E501
+
+
+@pytest.yield_fixture(scope='module')
+def event_loop(request):
+ """Override the default pytest event loop to allow for broaded scopedv fixtures."""
+ loop = asyncio.get_event_loop_policy().new_event_loop()
+ asyncio.set_event_loop(loop)
+ loop.set_debug(True)
+ yield loop
+ loop.close()
+ asyncio.set_event_loop(None)
+
+
+@pytest.fixture(scope='module')
+async def controller():
+ """Connect to the current controller."""
+ controller = Controller()
+ await controller.connect_current()
+ yield controller
+ await controller.disconnect()
+
+
+@pytest.fixture(scope='module')
+async def model(controller):
+ """Create a model that lives only for the duration of the test."""
+ model_name = "functest-{}".format(uuid.uuid4())
+ model = await controller.add_model(model_name)
+ yield model
+ await model.disconnect()
+ if os.getenv('test_preserve_model'):
+ return
+ await controller.destroy_model(model_name)
+ while model_name in await controller.list_models():
+ await asyncio.sleep(1)
+
+
+@pytest.fixture(scope='module')
+async def current_model():
+ """Return the current model, does not create or destroy it."""
+ model = Model()
+ await model.connect_current()
+ yield model
+ await model.disconnect()
+
+
+@pytest.fixture
+async def get_app(model):
+ """Return the application requested."""
+ async def _get_app(name):
+ try:
+ return model.applications[name]
+ except KeyError:
+ raise JujuError("Cannot find application {}".format(name))
+ return _get_app
+
+
+@pytest.fixture
+async def get_unit(model):
+ """Return the requested <app_name>/<unit_number> unit."""
+ async def _get_unit(name):
+ try:
+ (app_name, unit_number) = name.split('/')
+ return model.applications[app_name].units[unit_number]
+ except (KeyError, ValueError):
+ raise JujuError("Cannot find unit {}".format(name))
+ return _get_unit
+
+
+@pytest.fixture
+async def get_entity(model, get_unit, get_app):
+ """Return a unit or an application."""
+ async def _get_entity(name):
+ try:
+ return await get_unit(name)
+ except JujuError:
+ try:
+ return await get_app(name)
+ except JujuError:
+ raise JujuError("Cannot find entity {}".format(name))
+ return _get_entity
+
+
+@pytest.fixture
+async def run_command(get_unit):
+ """
+ Run a command on a unit.
+
+ :param cmd: Command to be run
+ :param target: Unit object or unit name string
+ """
+ async def _run_command(cmd, target):
+ unit = (
+ target
+ if type(target) is juju.unit.Unit
+ else await get_unit(target)
+ )
+ action = await unit.run(cmd)
+ return action.results
+ return _run_command
+
+
+@pytest.fixture
+async def file_stat(run_command):
+ """
+ Run stat on a file.
+
+ :param path: File path
+ :param target: Unit object or unit name string
+ """
+ async def _file_stat(path, target):
+ cmd = STAT_FILE % path
+ results = await run_command(cmd, target)
+ return json.loads(results['Stdout'])
+ return _file_stat
+
+
+@pytest.fixture
+async def file_contents(run_command):
+ """
+ Return the contents of a file.
+
+ :param path: File path
+ :param target: Unit object or unit name string
+ """
+ async def _file_contents(path, target):
+ cmd = 'cat {}'.format(path)
+ results = await run_command(cmd, target)
+ return results['Stdout']
+ return _file_contents
+
+
+@pytest.fixture
+async def reconfigure_app(get_app, model):
+ """Apply a different config to the requested app."""
+ async def _reconfigure_app(cfg, target):
+ application = (
+ target
+ if type(target) is juju.application.Application
+ else await get_app(target)
+ )
+ await application.set_config(cfg)
+ await application.get_config()
+ await model.block_until(lambda: application.status == 'active')
+ return _reconfigure_app
+
+
+@pytest.fixture
+async def create_group(run_command):
+ """Create the UNIX group specified."""
+ async def _create_group(group_name, target):
+ cmd = "sudo groupadd %s" % group_name
+ await run_command(cmd, target)
+ return _create_group
diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt
new file mode 100644
index 0000000..94307fc
--- /dev/null
+++ b/tests/functional/requirements.txt
@@ -0,0 +1,4 @@
+juju
+requests
+pytest
+pytest-asyncio
diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py
new file mode 100644
index 0000000..879d101
--- /dev/null
+++ b/tests/functional/test_deploy.py
@@ -0,0 +1,184 @@
+#!/usr/bin/python3.6
+
+import os
+
+import pytest
+
+import requests
+
+pytestmark = pytest.mark.asyncio
+
+CHARM_BUILD_DIR = os.getenv("CHARM_BUILD_DIR", "..").rstrip("/")
+
+SERIES = [
+ "trusty",
+ "xenial",
+ "bionic",
+]
+
+
+############
+# FIXTURES #
+############
+
+
+@pytest.fixture(scope='module', params=SERIES)
+def series(request):
+ """Return ubuntu version (i.e. xenial) in use in the test."""
+ return request.param
+
+
+@pytest.fixture(scope='module')
+async def deploy_relatives(model):
+ nrpe = "nrpe"
+ nrpe_app = await model.deploy(
+ 'cs:' + nrpe, application_name=nrpe,
+ series='trusty', config={},
+ num_units=0
+ )
+
+ mysql = "mysql"
+ mysql_app = await model.deploy(
+ 'cs:' + mysql, application_name=mysql,
+ series='trusty', config={}
+ )
+
+ mediawiki = "mediawiki"
+ mediawiki_app = await model.deploy(
+ 'cs:' + mediawiki, application_name=mediawiki,
+ series='trusty', config={}
+ )
+
+ await model.add_relation('mysql:db', 'mediawiki:db')
+ await model.add_relation('mysql:juju-info', 'nrpe:general-info')
+ await model.add_relation('mediawiki:juju-info', 'nrpe:general-info')
+ await model.block_until(
+ lambda: all(_.status == "active" for _ in (mysql_app, mediawiki_app))
+ )
+
+ yield {mediawiki: mediawiki_app, mysql: mysql_app, nrpe: nrpe_app}
+
+
+@pytest.fixture(scope='module')
+async def deploy_app(deploy_relatives, model, series):
+ """Return application of the charm under test."""
+ app_name = "nagios-{}".format(series)
+
+ """Deploy the nagios app."""
+ nagios_app = await model.deploy(
+ os.path.join(CHARM_BUILD_DIR, 'nagios'),
+ application_name=app_name,
+ series=series,
+ config={
+ 'enable_livestatus': True,
+ 'ssl': False
+ }
+ )
+ await model.add_relation('{}:monitors'.format(app_name), 'mysql:monitors')
+ await model.add_relation('{}:nagios'.format(app_name), 'mediawiki:juju-info')
+ await model.add_relation('nrpe:monitors', '{}:monitors'.format(app_name))
+ await model.block_until(lambda: nagios_app.status == "active")
+ await model.block_until(lambda: all(
+ _.status == "active"
+ for _ in list(deploy_relatives.values()) + [nagios_app]
+ ))
+ # no need to cleanup since the model will be be torn down at the end of the
+ # testing
+
+ yield nagios_app
+
+
+class Agent:
+ def __init__(self, unit):
+ self.u = unit
+ self.model = unit.model
+
+ def is_active(self, status):
+ u = self.u
+ return u.agent_status == status and u.workload_status == "active"
+
+ async def block_until(self, lambda_f, timeout=120, wait_period=5):
+ await self.model.block_until(
+ lambda_f, timeout=timeout, wait_period=wait_period
+ )
+
+
+@pytest.fixture()
+async def unit(model, deploy_app):
+ """Return the unit we've deployed."""
+ unit = Agent(deploy_app.units[0])
+ await unit.block_until(lambda: unit.is_active('idle'))
+ return unit
+
+
+@pytest.fixture()
+async def auth(file_contents, unit):
+ """Return the basic auth credentials."""
+ nagiospwd = await file_contents("/var/lib/juju/nagios.passwd", unit.u)
+ return 'nagiosadmin', nagiospwd.strip()
+
+
+#########
+# TESTS #
+#########
+
+async def test_status(deploy_app):
+ """Check that the app is in active state."""
+ assert deploy_app.status == "active"
+
+
+async def test_web_interface_is_protected(auth, unit):
+ """Check the nagios http interface."""
+ host_url = "http://%s/nagios3/" % unit.u.public_address
+ r = requests.get(host_url)
+ assert r.status_code == 401, "Web Interface is open to the world"
+
+ r = requests.get(host_url, auth=auth)
+ assert r.status_code == 200, "Web Admin login failed"
+
+
+async def test_hosts_being_monitored(auth, unit):
+ host_url = ("http://%s/cgi-bin/nagios3/status.cgi?"
+ "hostgroup=all&style=hostdetail") % unit.u.public_address
+ r = requests.get(host_url, auth=auth)
+ assert r.text.find('mysql') and r.text.find('mediawiki'), \
+ "Nagios is not monitoring the hosts it supposed to."
+
+
+@pytest.fixture(params=['on', 'only'])
+async def ssl(model, deploy_app, unit, request):
+ """
+ Enable SSL before a test, then disable after test
+
+ :param Model model: Current deployed model
+ :param Application deploy_app: Application under test
+ :param Agent unit: unit from the fixture
+ :param request: test parameters
+ """
+ await deploy_app.set_config({'ssl': request.param})
+ await unit.block_until(lambda: unit.is_active('executing'))
+ await unit.block_until(lambda: unit.is_active('idle'))
+ yield request.param
+ await deploy_app.set_config({'ssl': 'off'})
+ await unit.block_until(lambda: unit.is_active('executing'))
+ await unit.block_until(lambda: unit.is_active('idle'))
+
+
+async def test_web_interface_with_ssl(auth, unit, ssl):
+ http_url = "http://%s/nagios3/" % unit.u.public_address
+ if ssl == 'only':
+ """ SSL ONLY should prevent http nagios -- but must be a race in
+ my test conditions
+ with pytest.raises(requests.ConnectionError):
+ requests.get(http_url, auth=auth)
+ """
+ else:
+ r = requests.get(http_url, auth=auth)
+ assert r.status_code == 200, "HTTP Admin login failed"
+
+ https_url = "https://%s/nagios3/" % unit.u.public_address
+ r = requests.get(https_url, auth=auth, verify=False)
+ assert r.status_code == 200, "HTTPs Admin login failed"
+
+
+
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
new file mode 100644
index 0000000..f6fceac
--- /dev/null
+++ b/tests/unit/conftest.py
@@ -0,0 +1,5 @@
+import os
+import sys
+
+HOOKS = os.path.join(os.path.dirname(__file__), '..', '..', 'hooks')
+sys.path.append(HOOKS)
diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
new file mode 100644
index 0000000..df4d54b
--- /dev/null
+++ b/tests/unit/requirements.txt
@@ -0,0 +1,3 @@
+pytest
+pytest-cov
+pyyaml
\ No newline at end of file
diff --git a/tests/unit/test_common.py b/tests/unit/test_common.py
new file mode 100644
index 0000000..9833a02
--- /dev/null
+++ b/tests/unit/test_common.py
@@ -0,0 +1,5 @@
+import common
+
+
+def test_check_ip():
+ assert common.check_ip("1.2.3.4")
diff --git a/tests/unit/test_monitor_relation_changed.py b/tests/unit/test_monitor_relation_changed.py
new file mode 100644
index 0000000..3a7f5d6
--- /dev/null
+++ b/tests/unit/test_monitor_relation_changed.py
@@ -0,0 +1,7 @@
+import monitors_relation_changed
+
+
+def test_has_main():
+ # THIS IS A REALLY LAME TEST -- but it's a start for where there was nothing
+ # if you add tests later, please do better than me
+ assert hasattr(monitors_relation_changed, 'main')
diff --git a/tests/unit/test_website_relation_joined.py b/tests/unit/test_website_relation_joined.py
new file mode 100644
index 0000000..3164428
--- /dev/null
+++ b/tests/unit/test_website_relation_joined.py
@@ -0,0 +1,19 @@
+import unittest.mock as mock
+
+import pytest
+import website_relation_joined
+
+
+@mock.patch('common.get_local_ingress_address')
+@mock.patch('website_relation_joined.config')
+@mock.patch('website_relation_joined.relation_set')
+@pytest.mark.parametrize('ssl', [
+ ('only', 443),
+ ('on', 80),
+ ('off', 80)
+], ids=['ssl=only', 'ssl=on', 'ssl=off'])
+def test_main(relation_set, config, get_local_ingress_address, ssl):
+ get_local_ingress_address.return_value = 'example.com'
+ config.return_value = {'ssl': ssl[0]}
+ website_relation_joined.main()
+ relation_set.assert_called_with(None, port=ssl[1], hostname='example.com')
diff --git a/tox.ini b/tox.ini
index 18da390..34f9248 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
skipsdist=True
-;envlist = unit, functional
+envlist = lint, unit, functional
skip_missing_interpreters = True
[testenv]
@@ -8,32 +8,32 @@ basepython = python3
setenv =
PYTHONPATH = .
-;[testenv:unit]
-;commands =
-; {toxworkdir}/../tests/download_nagios_plugin3.py
-; pytest -v --ignore {toxinidir}/tests/functional \
-; --cov=lib \
-; --cov=reactive \
-; --cov=actions \
-; --cov-report=term \
-; --cov-report=annotate:reports/annotated \
-; --cov-report=html:reports/html
-;deps = -r{toxinidir}/tests/unit/requirements.txt
-; -r{toxinidir}/requirements.txt
-;setenv = PYTHONPATH={toxinidir}/lib:{envdir}/lib/python3.8
-;
-;[testenv:functional]
-;passenv =
-; HOME
-; CHARM_BUILD_DIR
-; PATH
-; PYTEST_KEEP_MODEL
-; PYTEST_CLOUD_NAME
-; PYTEST_CLOUD_REGION
-; PYTEST_MODEL
-;commands = pytest -v --ignore {toxinidir}/tests/unit
-;deps = -r{toxinidir}/tests/functional/requirements.txt
-; -r{toxinidir}/requirements.txt
+[testenv:unit]
+commands =
+ pytest -v --ignore {toxinidir}/tests/functional \
+ --cov=hooks \
+ --cov-report=term \
+ --cov-report=annotate:reports/annotated \
+ --cov-report=html:reports/html
+deps = -r{toxinidir}/tests/unit/requirements.txt
+ -r{toxinidir}/requirements.txt
+
+
+[coverage:run]
+omit = hooks/charmhelpers/*
+
+[testenv:functional]
+passenv =
+ HOME
+ CHARM_BUILD_DIR
+ PATH
+ PYTEST_KEEP_MODEL
+ PYTEST_CLOUD_NAME
+ PYTEST_CLOUD_REGION
+ PYTEST_MODEL
+commands = pytest -v --ignore {toxinidir}/tests/unit
+deps = -r{toxinidir}/tests/functional/requirements.txt
+ -r{toxinidir}/requirements.txt
[testenv:lint]
commands = flake8
Follow ups