Merge ~aluria/charm-openstack-service-checks/+git/charm-openstack-service-checks:bug/1821316-rewrite into ~canonical-bootstack/charm-openstack-service-checks:master

Proposed by Alvaro Uria
Status: Merged
Approved by: Alvaro Uria
Approved revision: dd64777995c0e26d4f85e30d75546f4c7db404fe
Merged at revision: 1c0564e386ee87e22b0dfb00b1a3f9c05c09a63d
Proposed branch: ~aluria/charm-openstack-service-checks/+git/charm-openstack-service-checks:bug/1821316-rewrite
Merge into: ~canonical-bootstack/charm-openstack-service-checks:master
Diff against target: 1570 lines (+1018/-72)
18 files modified
.gitignore (+22/-10)
Makefile (+47/-18)
config.yaml (+1/-0)
dev/null (+0/-9)
files/plugins/check_nova_services.py (+1/-2)
interfaces/.empty (+1/-0)
layer.yaml (+5/-0)
layers/.empty (+1/-0)
lib/lib_openstack_service_checks.py (+247/-0)
metadata.yaml (+3/-4)
reactive/openstack_service_checks.py (+248/-0)
tests/functional/conftest.py (+179/-0)
tests/functional/requirements.txt (+6/-0)
tests/functional/test_deploy.py (+92/-0)
tests/unit/conftest.py (+77/-0)
tests/unit/requirements.txt (+10/-0)
tests/unit/test_lib.py (+44/-0)
tox.ini (+34/-29)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Legacy - Canonical WTFB Pending
Review via email: mp+365243@code.launchpad.net

Commit message

Rewrite to integrate template-python-pytest code and approach

* separation between charm logic (reactive) and helper functions (lib)
* improvement on charm states and messages
* unit and functional tests added (using pytest framework)

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Unable to determine commit message from repository - please click "Set commit message" and enter the commit message manually.

Revision history for this message
Alvaro Uria (aluria) wrote :

Code has already been reviewed at [1][2][3].

I have also tested a merge to master, and as we see in this MP, 2 minor conflicts were raised:
a) config.yaml: swift_check_params didn't have a default value, and 'default: "/"' needs to be resolved (plus run "git add config.yaml" to resolve the conflict).
b) reactive/service_checks.py: script has been renamed (and refactored) into an already existing script (reactive/openstack_service_checks.py). We need to run "git rm reactive/service_checks.py"

Once both conflicts are resolved, run "git merge --continue" and that should be it.

1. https://code.launchpad.net/~aluria/charm-openstack-service-checks/+git/charm-openstack-service-checks/+merge/364971
2. https://code.launchpad.net/~aluria/charm-openstack-service-checks/+git/charm-openstack-service-checks/+merge/364970
3. https://code.launchpad.net/~aluria/charm-openstack-service-checks/+git/charm-openstack-service-checks/+merge/364969

Revision history for this message
Alvaro Uria (aluria) wrote :

The purpose of this MP is to:
1) move all code off "src/" folder, per latest template-python-pytest revision
2) update tox.ini and Makefile with new environment variables (PYTEST_KEEP_MODEL vs old TEST_PRESERVE_MODEL), as well as update functional/conftest.py, etc. per latest revision (note functional/test_deploy.py has been kept as is, for now).

Revision history for this message
Stuart Bishop (stub) wrote :

Yup.

The conflicts need to be resolved on this branch. Merge in master, resolve the conflicts, and push. Wait a few seconds, reload this page, set the MP status to 'Approved' and mergebot will be able to land.

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change has no commit message, setting status to needs review.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 1c0564e386ee87e22b0dfb00b1a3f9c05c09a63d

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index dccc0dc..6a23b4b 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -1,10 +1,22 @@
6-*~
7-/bin
8-debian/files
9-/pkg
10-*.pyc
11-__pycache__
12-*.swp
13-.tox
14-.venv
15-.idea
16+# Byte-compiled / optimized / DLL files
17+__pycache__/
18+*.py[cod]
19+*$py.class
20+
21+# Log files
22+*.log
23+
24+.tox/
25+.coverage
26+
27+# vi
28+.*.swp
29+
30+# pycharm
31+.idea/
32+
33+# version data
34+repo-info
35+
36+# reports
37+reports/*
38diff --git a/Makefile b/Makefile
39index 27eeff6..4dc77e9 100644
40--- a/Makefile
41+++ b/Makefile
42@@ -1,26 +1,55 @@
43-#!/usr/bin/make
44+PROJECTPATH = $(dir $(realpath $(firstword $(MAKEFILE_LIST))))
45+DIRNAME = $(notdir $(PROJECTPATH:%/=%))
46
47-all: lint test
48+ifndef CHARM_BUILD_DIR
49+ CHARM_BUILD_DIR := /tmp/$(DIRNAME)-builds
50+ $(warning Warning CHARM_BUILD_DIR was not set, defaulting to $(CHARM_BUILD_DIR))
51+endif
52
53+help:
54+ @echo "This project supports the following targets"
55+ @echo ""
56+ @echo " make help - show this text"
57+ @echo " make lint - run flake8"
58+ @echo " make test - run the unittests and lint"
59+ @echo " make unittest - run the tests defined in the unittest subdirectory"
60+ @echo " make functional - run the tests defined in the functional subdirectory"
61+ @echo " make release - build the charm"
62+ @echo " make clean - remove unneeded files"
63+ @echo ""
64
65-.PHONY: clean
66-clean:
67- @rm -rf .tox
68+lint:
69+ @echo "Running flake8"
70+ @tox -e lint
71
72-.PHONY: apt_prereqs
73-apt_prereqs:
74- @# Need tox, but don't install the apt version unless we have to (don't want to conflict with pip)
75- @which tox >/dev/null || (sudo apt-get install -y python3-pip && sudo pip3 install tox)
76+test: lint unittest functional
77
78-.PHONY: lint
79-lint: apt_prereqs
80- @tox -e pep8
81- @charm proof
82+unittest:
83+ @tox -e unit
84
85-.PHONY: test
86-unit_test: apt_prereqs
87- @echo Starting tests...
88- tox
89+functional: build
90+ @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \
91+ PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \
92+ PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \
93+ CHARM_BUILD_DIR=$(CHARM_BUILD_DIR) \
94+ tox -e functional
95
96 build:
97- charm build
98+ @echo "Building charm to base directory $(CHARM_BUILD_DIR)"
99+ @CHARM_LAYERS_DIR=./layers \
100+ CHARM_INTERFACES_DIR=./interfaces \
101+ TERM=linux \
102+ CHARM_BUILD_DIR=$(CHARM_BUILD_DIR) \
103+ charm build . --force
104+
105+release: clean build
106+ @echo "Charm is built at $(CHARM_BUILD_DIR)"
107+
108+clean:
109+ @echo "Cleaning files"
110+ @if [ -d $(CHARM_BUILD_DIR) ] ; then rm -r $(CHARM_BUILD_DIR) ; fi
111+ @if [ -d $(PROJECTPATH)/.tox ] ; then rm -r $(PROJECTPATH)/.tox ; fi
112+ @if [ -d $(PROJECTPATH)/.pytest_cache ] ; then rm -r $(PROJECTPATH)/.pytest_cache ; fi
113+
114+# The targets below don't depend on a file
115+.PHONY: lint test unittest functional build release clean help
116diff --git a/config.yaml b/config.yaml
117index b153340..6f09823 100644
118--- a/config.yaml
119+++ b/config.yaml
120@@ -75,6 +75,7 @@ options:
121 If true, create NRPE checks matching all 'admin' URLs in the Keystone catalog.
122 swift_check_params:
123 type: string
124+ default: "/"
125 description: |
126 URL to use with check_http if there is a Swift endpoint. Default is '/', but it's possible to add extra params,
127 e.g. '/v3 -e Unauthorized -d x-openstack-request-id' or a different url, e.g. '/healthcheck'. Mitaka Swift
128diff --git a/files/plugins/check_nova_services.py b/files/plugins/check_nova_services.py
129index 6498542..8a3666d 100755
130--- a/files/plugins/check_nova_services.py
131+++ b/files/plugins/check_nova_services.py
132@@ -75,8 +75,7 @@ def check_nova_services(args, nova):
133 if agg['critical']])
134 status_warn = len([agg['warning'] for agg in status
135 if agg['warning']])
136- for x in status:
137- (msg.append(x['msg_text']) if x['msg_text'] is not '' else None)
138+ msg.extend([x['msg_text'] for x in status if x['msg_text'] != ''])
139 if status_crit:
140 output = "CRITICAL: {}".format(", ".join(msg))
141 raise nagios_plugin.CriticalError(output)
142diff --git a/interfaces/.empty b/interfaces/.empty
143new file mode 100644
144index 0000000..792d600
145--- /dev/null
146+++ b/interfaces/.empty
147@@ -0,0 +1 @@
148+#
149diff --git a/layer.yaml b/layer.yaml
150index 92676ed..290b275 100644
151--- a/layer.yaml
152+++ b/layer.yaml
153@@ -1,3 +1,8 @@
154+# exclude the interfaces and layers folders we use for submodules
155+exclude:
156+ - interfaces
157+ - layers
158+# include required layers here
159 includes:
160 - 'layer:basic'
161 - 'interface:keystone-credentials'
162diff --git a/layers/.empty b/layers/.empty
163new file mode 100644
164index 0000000..792d600
165--- /dev/null
166+++ b/layers/.empty
167@@ -0,0 +1 @@
168+#
169diff --git a/lib/lib_openstack_service_checks.py b/lib/lib_openstack_service_checks.py
170new file mode 100644
171index 0000000..563d09c
172--- /dev/null
173+++ b/lib/lib_openstack_service_checks.py
174@@ -0,0 +1,247 @@
175+import os
176+from urllib.parse import urlparse
177+
178+from charmhelpers.core.templating import render
179+from charmhelpers.contrib.openstack.utils import config_flags_parser
180+from charmhelpers.core import hookenv, host, unitdata
181+from charmhelpers.contrib.charmsupport.nrpe import NRPE
182+import keystoneauth1
183+from keystoneclient import session
184+
185+
186+class OSCCredentialsError(Exception):
187+ pass
188+
189+
190+class OSCEndpointError(OSCCredentialsError):
191+ pass
192+
193+
194+class OSCHelper():
195+ def __init__(self):
196+ self.charm_config = hookenv.config()
197+
198+ def store_keystone_credentials(self, creds):
199+ '''store keystone credentials'''
200+ unitdata.kv().set('keystonecreds', creds)
201+ return
202+
203+ @property
204+ def novarc(self):
205+ return '/var/lib/nagios/nagios.novarc'
206+
207+ @property
208+ def plugins_dir(self):
209+ return '/usr/local/lib/nagios/plugins/'
210+
211+ def get_os_credentials(self):
212+ ident_creds = config_flags_parser(self.charm_config['os-credentials'])
213+ if not ident_creds.get('auth_url'):
214+ raise OSCCredentialsError('auth_url')
215+ elif '/v3' in ident_creds.get('auth_url'):
216+ extra_attrs = ['domain']
217+ creds = {'auth_version': 3}
218+ else:
219+ extra_attrs = []
220+ creds = {}
221+
222+ common_attrs = ('username password region_name auth_url'
223+ ' credentials_project').split()
224+ all_attrs = common_attrs + extra_attrs
225+ missing = [k for k in all_attrs if k not in ident_creds]
226+ if missing:
227+ raise OSCCredentialsError(', '.join(missing))
228+
229+ ident_creds['auth_url'] = ident_creds['auth_url'].strip('\"\'')
230+ creds.update(dict([(k, ident_creds.get(k))
231+ for k in all_attrs
232+ if k not in ('credentials_project', 'domain')]))
233+ if extra_attrs:
234+ creds.update({'project_name': ident_creds['credentials_project'],
235+ 'user_domain_name': ident_creds['domain'],
236+ 'project_domain_name': ident_creds['domain'],
237+ })
238+ else:
239+ creds['tenant_name'] = ident_creds['credentials_project']
240+
241+ return creds
242+
243+ def get_keystone_credentials(self):
244+ '''retrieve keystone credentials from either config or relation data
245+
246+ If config 'os-crendentials' is set, return that info otherwise look for a keystonecreds relation data'
247+
248+ :return: dict of credential information for keystone
249+ '''
250+ return unitdata.kv().get('keystonecreds')
251+
252+ @property
253+ def nova_warn(self):
254+ return self.charm_config.get('nova_warn')
255+
256+ @property
257+ def nova_crit(self):
258+ return self.charm_config.get('nova_crit')
259+
260+ @property
261+ def skip_disabled(self):
262+ if self.charm_config.get('skip-disabled'):
263+ return '--skip-disabled'
264+ else:
265+ return ''
266+
267+ @property
268+ def check_dns(self):
269+ return self.charm_config.get('check-dns')
270+
271+ def render_checks(self, creds):
272+ render(source='nagios.novarc', target=self.novarc, context=creds,
273+ owner='nagios', group='nagios')
274+
275+ nrpe = NRPE()
276+ if not os.path.exists(self.plugins_dir):
277+ os.makedirs(self.plugins_dir)
278+
279+ charm_plugin_dir = os.path.join(hookenv.charm_dir(),
280+ 'files',
281+ 'plugins/')
282+ host.rsync(charm_plugin_dir,
283+ self.plugins_dir,
284+ options=['--executability'])
285+
286+ nova_check_command = os.path.join(self.plugins_dir,
287+ 'check_nova_services.py')
288+ check_command = '{} --warn {} --crit {} {}'.format(
289+ nova_check_command, self.nova_warn, self.nova_crit,
290+ self.skip_disabled).strip()
291+ nrpe.add_check(shortname='nova_services',
292+ description='Check that enabled Nova services are up',
293+ check_cmd=check_command,
294+ )
295+
296+ nrpe.add_check(shortname='neutron_agents',
297+ description='Check that enabled Neutron agents are up',
298+ check_cmd=os.path.join(self.plugins_dir,
299+ 'check_neutron_agents.sh'),
300+ )
301+
302+ if len(self.check_dns):
303+ nrpe.add_check(shortname='dns_multi',
304+ description='Check DNS names are resolvable',
305+ check_cmd='{} {}'.format(
306+ os.path.join(self.plugins_dir,
307+ 'check_dns_multi.sh'),
308+ ' '.join(self.check_dns.split())),
309+ )
310+ else:
311+ nrpe.remove_check(shortname='dns_multi')
312+ nrpe.write()
313+
314+ self.create_endpoint_checks(creds)
315+
316+ def create_endpoint_checks(self, creds):
317+ """
318+ Create an NRPE check for each Keystone catalog endpoint.
319+
320+ Read the Keystone catalog, and create a check for each endpoint listed.
321+ If there is a healthcheck endpoint for the API, use that URL, otherwise check
322+ the url '/'.
323+ If SSL, add a check for the cert.
324+
325+ v2 endpoint needs the 'interface' attribute:
326+ <Endpoint {'id': 'XXXXX', 'region': 'RegionOne', 'publicurl': 'http://10.x.x.x:9696',
327+ 'service_id': 'YYY', 'internalurl': 'http://10.x.x.x:9696', 'enabled': True,
328+ 'adminurl': 'http://10.x.x.x:9696'}>
329+ """
330+ # provide URLs that can be used for healthcheck for some services
331+ # This also provides a nasty hack-ish way to add switches if we need
332+ # for some services.
333+ health_check_params = {
334+ 'keystone': '/healthcheck',
335+ 's3': '/healthcheck',
336+ 'aodh': '/healthcheck',
337+ 'ceilometer': '/ -e Unauthorized -d x-openstack-request-id',
338+ 'cinderv3': '/v3 -e Unauthorized -d x-openstack-request-id',
339+ 'cinderv2': '/v2 -e Unauthorized -d x-openstack-request-id',
340+ 'cinderv1': '/v1 -e Unauthorized -d x-openstack-request-id',
341+ 'glance': '/healthcheck',
342+ 'nova': '/healthcheck',
343+ 'placement': '/healthcheck -e Unauthorized -d x-openstack-request-id',
344+ 'swift': self.charm_config.get('swift_check_params', '/'),
345+ }
346+ keystone_client = self.get_keystone_client(creds)
347+ try:
348+ endpoints = keystone_client.endpoints.list()
349+ except keystoneauth1.exceptions.http.InternalServerError as error:
350+ raise OSCEndpointError(
351+ 'Unable to list the keystone endpoints, yet: {}'.format(error))
352+
353+ services = [x for x in keystone_client.services.list() if x.enabled]
354+ nrpe = NRPE()
355+ skip_service = set()
356+ for endpoint in endpoints:
357+ endpoint.service_names = [x.name
358+ for x in services
359+ if x.id == endpoint.service_id]
360+ service_name = endpoint.service_names[0]
361+ endpoint.healthcheck_url = health_check_params.get(service_name, '/')
362+ if not hasattr(endpoint, 'interface'):
363+ if service_name == 'keystone':
364+ # Note(aluria): filter:healthcheck is not configured in v2
365+ # https://docs.openstack.org/keystone/pike/configuration.html#health-check-middleware
366+ continue
367+ for interface in 'admin internal public'.split():
368+ old_interface_name = '{}url'.format(interface)
369+ if not hasattr(endpoint, old_interface_name):
370+ continue
371+ endpoint.interface = interface
372+ endpoint.url = getattr(endpoint, old_interface_name)
373+ skip_service.add(service_name)
374+ break
375+
376+ if self.charm_config.get('check_{}_urls'.format(endpoint.interface)):
377+ cmd_params = ['/usr/lib/nagios/plugins/check_http']
378+ check_url = urlparse(endpoint.url)
379+ host, port = check_url.netloc.split(':')
380+ cmd_params.append('-H {} -p {}'.format(host, port))
381+ cmd_params.append('-u {}'.format(endpoint.healthcheck_url))
382+ # if this is https, we want to add a check for cert expiry
383+ # also need to tell check_http use use TLS
384+ if check_url.scheme == 'https':
385+ cmd_params.append('-S')
386+ # Add an extra check for TLS cert expiry
387+ cmd_params.append('-C {},{}'.format(self.charm_config['tls_warn_days'] or 30,
388+ self.charm_config['tls_crit_days'] or 14))
389+ nrpe.add_check(shortname='{}_{}_cert'.format(service_name, endpoint.interface),
390+ description='Certificate expiry check for {} {}'.format(service_name,
391+ endpoint.interface),
392+ check_cmd=' '.join(cmd_params))
393+
394+ # Add the actual health check for the URL
395+ nrpe.add_check(shortname='{}_{}'.format(service_name, endpoint.interface),
396+ description='Endpoint url check for {} {}'.format(service_name, endpoint.interface),
397+ check_cmd=' '.join(cmd_params))
398+ nrpe.write()
399+
400+ def get_keystone_client(self, creds):
401+ """Import the appropriate Keystone client depending on API version.
402+
403+ Use credential info to determine the Keystone API version, and make a
404+ client session object that is to be used for authenticated
405+ communication with Keystone.
406+
407+ :returns: a keystoneclient Client object
408+ """
409+ if int(creds.get('auth_version', 0)) >= 3:
410+ from keystoneclient.v3 import client
411+ from keystoneclient.auth.identity import v3 as kst_version
412+ auth_fields = 'username password auth_url user_domain_name project_domain_name project_name'.split()
413+ else:
414+ from keystoneclient.v2_0 import client
415+ from keystoneclient.auth.identity import v2 as kst_version
416+ auth_fields = 'username password auth_url tenant_name'.split()
417+
418+ auth_creds = dict([(key, creds.get(key)) for key in auth_fields])
419+ auth = kst_version.Password(**auth_creds)
420+ sess = session.Session(auth=auth)
421+ return client.Client(session=sess)
422diff --git a/metadata.yaml b/metadata.yaml
423index 96c1f7e..3ca8dc0 100644
424--- a/metadata.yaml
425+++ b/metadata.yaml
426@@ -1,17 +1,16 @@
427 name: openstack-service-checks
428 summary: OpenStack Services NRPE Checks
429-description: |
430- OpenStack Services NRPE Checks
431-maintainer: Jill Rouleau <jill.rouleau@canonical.com>
432+description: OpenStack Services NRPE Checks
433+maintainer: Nagios Charm Developers <nagios-charmers@lists.launchpad.net>
434 subordinate: false
435 tags:
436 - openstack
437 - ops
438 - monitoring
439 series:
440+- bionic
441 - xenial
442 - trusty
443-- bionic
444 provides:
445 nrpe-external-master:
446 interface: nrpe-external-master
447diff --git a/reactive/openstack_service_checks.py b/reactive/openstack_service_checks.py
448new file mode 100644
449index 0000000..c122bf1
450--- /dev/null
451+++ b/reactive/openstack_service_checks.py
452@@ -0,0 +1,248 @@
453+# -*- coding: us-ascii -*-
454+"""
455+install_osc +------------------------> render_config+--------------> do_restart
456+ ^ + ^
457+conf_kst_user+---> save_creds +-----------+ v |
458+ create_endpoints +-----+
459+
460+Available states:
461+ identity-credentials.available: kst creds available
462+ identity-credentials.connected: kst relation joined
463+ nrpe-external-master.available: nrpe relation joined
464+ openstack-service-checks.configured: render_config allowed
465+ openstack-service-checks.endpoints.configured: create_endpoints allowed
466+ openstack-service-checks.installed: install_osc entrypoint
467+ openstack-service-checks.started: if not set, restart nagios-nrpe-server
468+ openstack-service-checks.stored-creds: kst creds available for the unit
469+"""
470+import base64
471+import subprocess
472+
473+from charmhelpers.core import hookenv, host, unitdata
474+from charms.reactive import clear_flag, set_flag, when, when_not
475+
476+from lib_openstack_service_checks import (
477+ OSCHelper,
478+ OSCCredentialsError,
479+ OSCEndpointError
480+)
481+
482+CERT_FILE = '/usr/local/share/ca-certificates/openstack-service-checks.crt'
483+helper = OSCHelper()
484+
485+
486+@when_not('openstack-service-checks.installed')
487+@when('nrpe-external-master.available')
488+def install_openstack_service_checks():
489+ """Entry point to start configuring the unit
490+
491+ Triggered if related to the nrpe-external-master relation.
492+ Some relation data can be initialized if the application is related to
493+ keystone.
494+ """
495+ set_flag('openstack-service-checks.installed')
496+ clear_flag('openstack-service-checks.configured')
497+
498+
499+@when_not('identity-credentials.available')
500+@when('identity-credentials.connected')
501+def configure_ident_username(keystone):
502+ """Requests a user to the Identity Service
503+ """
504+ username = 'nagios'
505+ keystone.request_credentials(username)
506+ clear_flag('openstack-service-checks.stored-creds')
507+
508+
509+@when_not('openstack-service-checks.stored-creds')
510+@when('identity-credentials.available')
511+def save_creds(keystone):
512+ """Collect and save credentials from Keystone relation.
513+
514+ Get credentials from the Keystone relation,
515+ reformat them into something the Keystone client can use, and
516+ save them into the unitdata.
517+ """
518+ creds = {'username': keystone.credentials_username(),
519+ 'password': keystone.credentials_password(),
520+ 'region': keystone.region(),
521+ }
522+ if keystone.api_version() == '3':
523+ api_url = 'v3'
524+ try:
525+ domain = keystone.domain()
526+ except AttributeError:
527+ domain = 'service_domain'
528+ # keystone relation sends info back with funny names, fix here
529+ creds.update({'project_name': keystone.credentials_project(),
530+ 'auth_version': '3',
531+ 'user_domain_name': domain,
532+ 'project_domain_name': domain})
533+ else:
534+ api_url = 'v2.0'
535+ creds['tenant_name'] = keystone.credentials_project()
536+
537+ creds['auth_url'] = '{proto}://{host}:{port}/{api_url}'.format(
538+ proto=keystone.auth_protocol(), host=keystone.auth_host(),
539+ port=keystone.auth_port(), api_url=api_url)
540+
541+ helper.store_keystone_credentials(creds)
542+ set_flag('openstack-service-checks.stored-creds')
543+ clear_flag('openstack-service-checks.configured')
544+
545+
546+@when_not('identity-credentials.connected')
547+@when_not('identity-credentials.available')
548+@when('openstack-service-checks.stored-creds')
549+def allow_keystone_store_overwrite():
550+ """Allow unitdata overwrite if keystone relation is recycled.
551+ """
552+ clear_flag('openstack-service-checks.stored-creds')
553+
554+
555+def get_credentials():
556+ """Get credential info from either config or relation data
557+
558+ If config 'os-credentials' is set, return it. Otherwise look for a
559+ keystonecreds relation data.
560+ """
561+ try:
562+ creds = helper.get_os_credentials()
563+ except OSCCredentialsError as error:
564+ creds = helper.get_keystone_credentials()
565+ if not creds:
566+ hookenv.log('render_config: No credentials yet, skipping')
567+ hookenv.status_set('blocked',
568+ 'Missing os-credentials vars: {}'.format(error))
569+ return
570+ return creds
571+
572+
573+@when('openstack-service-checks.installed')
574+@when_not('openstack-service-checks.configured')
575+def render_config():
576+ """Render nrpe checks from the templates
577+
578+ This code is only triggered after the nrpe relation is set. If a relation
579+ with keystone is later set, it will be re-triggered. On the other hand,
580+ if a keystone relation exists but not a nrpe relation, it won't be run.
581+
582+ Furthermore, juju config os-credentials take precedence over keystone
583+ related data.
584+ """
585+ def block_tls_failure(error):
586+ hookenv.log('update-ca-certificates failed: {}'.format(error),
587+ hookenv.ERROR)
588+ hookenv.status_set('blocked',
589+ 'update-ca-certificates error. check logs')
590+ return
591+
592+ creds = get_credentials()
593+ if not creds:
594+ return
595+
596+ # Fix TLS
597+ if helper.charm_config['trusted_ssl_ca'].strip():
598+ trusted_ssl_ca = helper.charm_config['trusted_ssl_ca'].strip()
599+ hookenv.log('Writing ssl ca cert:{}'.format(trusted_ssl_ca))
600+ cert_content = base64.b64decode(trusted_ssl_ca).decode()
601+ try:
602+ with open(CERT_FILE, 'w') as fd:
603+ fd.write(cert_content)
604+ subprocess.call(['/usr/sbin/update-ca-certificates'])
605+
606+ except subprocess.CalledProcessError as error:
607+ block_tls_failure(error)
608+ return
609+ except PermissionError as error:
610+ block_tls_failure(error)
611+ return
612+
613+ hookenv.log('render_config: Got credentials for'
614+ ' username={}'.format(creds.get('username')))
615+
616+ try:
617+ helper.render_checks(creds)
618+ set_flag('openstack-service-checks.endpoints.configured')
619+ except OSCEndpointError as error:
620+ hookenv.log(error)
621+
622+ set_flag('openstack-service-checks.configured')
623+ clear_flag('openstack-service-checks.started')
624+
625+
626+@when('openstack-service-checks.installed')
627+@when('openstack-service-checks.configured')
628+@when_not('openstack-service-checks.endpoints.configured')
629+def configure_nrpe_endpoints():
630+ """Create an NRPE check for each Keystone catalog endpoint.
631+
632+ Read the Keystone catalog, and create a check for each endpoint listed.
633+ If there is a healthcheck endpoint for the API, use that URL. Otherwise,
634+ check the url '/'.
635+
636+ If TLS is enabled, add a check for the cert.
637+ """
638+ creds = get_credentials()
639+ if not creds:
640+ return
641+
642+ try:
643+ helper.create_endpoint_checks(creds)
644+ set_flag('openstack-service-checks.endpoints.configured')
645+ clear_flag('openstack-service-checks.started')
646+ except OSCEndpointError as error:
647+ hookenv.log(error)
648+
649+
650+@when('openstack-service-checks.configured')
651+@when_not('openstack-service-checks.started')
652+def do_restart():
653+ hookenv.log('Reloading nagios-nrpe-server')
654+ host.service_restart('nagios-nrpe-server')
655+ hookenv.status_set('active', 'Unit is ready')
656+ set_flag('openstack-service-checks.started')
657+
658+
659+@when('config.changed.os-credentials')
660+@when('nrpe-external-master.available')
661+def do_reconfigure_nrpe():
662+ clear_flag('openstack-service-checks.configured')
663+ clear_flag('openstack-service-checks.endpoints.configured')
664+
665+
666+@when_not('nrpe-external-master.available')
667+def missing_nrpe():
668+ """Avoid a user action to be missed or overwritten by another hook
669+ """
670+ if hookenv.hook_name() != 'update-status':
671+ hookenv.status_set('blocked', 'Missing relations: nrpe')
672+
673+
674+@when('openstack-service-checks.installed')
675+@when('nrpe-external-master.available')
676+def parse_hooks():
677+ if hookenv.hook_name() == 'upgrade-charm':
678+ # Check if creds storage needs to be migrated
679+ # Old key: keystone-relation-creds
680+ # New key: keystonecreds
681+ kv = unitdata.kv()
682+ creds = kv.get('keystonecreds')
683+ old_creds = kv.get('keystone-relation-creds')
684+ if old_creds and not creds:
685+ # This set of creds needs an update to a newer format
686+ creds = {
687+ 'username': old_creds['credentials_username'],
688+ 'password': old_creds['credentials_password'],
689+ 'project_name': old_creds['credentials_project'],
690+ 'tenant_name': old_creds['credentials_project'],
691+ 'user_domain_name': old_creds.get('credentials_user_domain'),
692+ 'project_domain_name': old_creds.get('credentials_project_domain'),
693+ }
694+ kv.set('keystonecreds', creds)
695+
696+ if old_creds:
697+ kv.unset('keystone-relation-creds')
698+
699+ # render configs again
700+ do_reconfigure_nrpe()
701diff --git a/reactive/service_checks.py b/reactive/service_checks.py
702deleted file mode 100644
703index 1a6f5bb..0000000
704--- a/reactive/service_checks.py
705+++ /dev/null
706@@ -1,326 +0,0 @@
707-from __future__ import print_function
708-import base64
709-import keystoneauth1
710-import os
711-import subprocess
712-from charms.reactive import (
713- when,
714- when_not,
715- set_state,
716- remove_state,
717-)
718-
719-from charmhelpers.core.templating import render
720-from charmhelpers.contrib.openstack.utils import config_flags_parser
721-from charmhelpers.core import (
722- host,
723- hookenv,
724- unitdata,
725-)
726-
727-from charmhelpers.contrib.charmsupport.nrpe import NRPE
728-from urllib.parse import urlparse
729-
730-config = hookenv.config()
731-NOVARC = '/var/lib/nagios/nagios.novarc'
732-PLUGINS_DIR = '/usr/local/lib/nagios/plugins/'
733-
734-
735-@when_not('os-service-checks.installed')
736-def install_service_checks():
737- # Apt package installs are handled by the basic layer
738- set_state('os-service-checks.installed')
739- remove_state('os-service-checks.configured')
740- hookenv.status_set('active', 'Ready')
741-
742-
743-@when('identity-credentials.connected')
744-def configure_ident_username(keystone):
745- username = 'nagios'
746- keystone.request_credentials(username)
747-
748-
749-@when('identity-credentials.available')
750-def save_creds(keystone):
751- """Collect and save credentials from Keystone relation.
752-
753- Get credentials from the Keystone relation,
754- reformat them into something the Keystone client
755- can use, and save them into the unitdata.
756- """
757- creds = {
758- 'username': keystone.credentials_username(),
759- 'password': keystone.credentials_password(),
760- 'region': keystone.region(),
761- }
762- if keystone.api_version() == '3':
763- api_url = "v3"
764- try:
765- domain = keystone.domain()
766- except AttributeError:
767- domain = 'service_domain'
768- # keystone relation sends info back with funny names, fix here
769- creds.update({
770- 'project_name': keystone.credentials_project(),
771- 'auth_version': '3',
772- 'user_domain_name': domain,
773- 'project_domain_name': domain
774- })
775- else:
776- api_url = "v2.0"
777- creds['tenant_name'] = keystone.credentials_project()
778-
779- auth_url = "%s://%s:%s/%s" % (keystone.auth_protocol(),
780- keystone.auth_host(), keystone.auth_port(),
781- api_url)
782- creds['auth_url'] = auth_url
783- unitdata.kv().set('keystonecreds', creds)
784- remove_state('os-service-checks.configured')
785-
786-
787-# allow user to override credentials (and the need to be related to Keystone)
788-# with 'os-credentials'
789-def get_credentials():
790- """Get credential info from either config or relation data.
791-
792- If config 'os-credentials' is set, return that info otherwise look for for a keystonecreds relation data.
793-
794- :return: dictionary of credential information for Keystone.
795- """
796- ident_creds = config_flags_parser(config.get('os-credentials'))
797- if ident_creds:
798- creds = {
799- 'username': ident_creds['username'],
800- 'password': ident_creds['password'],
801- 'region': ident_creds['region_name'],
802- 'auth_url': ident_creds['auth_url'].strip('\"\''),
803- }
804- if '/v3' in ident_creds['auth_url']:
805- creds.update({
806- 'project_name': ident_creds['credentials_project'],
807- 'auth_version': '3',
808- 'user_domain_name': ident_creds['domain'],
809- 'project_domain_name': ident_creds['domain'],
810- })
811- else:
812- creds['tenant_name'] = ident_creds['credentials_project']
813- else:
814- kv = unitdata.kv()
815- creds = kv.get('keystonecreds')
816- old_creds = kv.get('keystone-relation-creds')
817- if old_creds and not creds:
818- # This set of creds needs an update to a newer format
819- creds['username'] = old_creds.pop('credentials_username')
820- creds['password'] = old_creds.pop('credentials_password')
821- creds['project_name'] = old_creds.pop('credentials_project')
822- creds['tenant_name'] = old_creds['project_name']
823- creds['user_domain_name'] = old_creds.pop('credentials_user_domain', None)
824- creds['project_domain_name'] = old_creds.pop('credentials_project_domain', None)
825- kv.set('keystonecreds', creds)
826- kv.update(creds, 'keystonecreds')
827- return creds
828-
829-
830-def render_checks():
831- nrpe = NRPE()
832- if not os.path.exists(PLUGINS_DIR):
833- os.makedirs(PLUGINS_DIR)
834- charm_file_dir = os.path.join(hookenv.charm_dir(), 'files')
835- charm_plugin_dir = os.path.join(charm_file_dir, 'plugins')
836- host.rsync(
837- charm_plugin_dir,
838- '/usr/local/lib/nagios/',
839- options=['--executability']
840- )
841-
842- warn = config.get("nova_warn")
843- crit = config.get("nova_crit")
844- skip_disabled = config.get("skip-disabled")
845- check_dns = config.get("check-dns")
846- nova_check_command = os.path.join(PLUGINS_DIR, 'check_nova_services.py')
847- check_command = '{} --warn {} --crit {}'.format(nova_check_command, warn, crit)
848-
849- if skip_disabled:
850- check_command = check_command + ' --skip-disabled'
851-
852- nrpe.add_check(shortname='nova_services',
853- description='Check that enabled Nova services are up',
854- check_cmd=check_command)
855-
856- nrpe.add_check(shortname='neutron_agents',
857- description='Check that enabled Neutron agents are up',
858- check_cmd=os.path.join(PLUGINS_DIR, 'check_neutron_agents.sh'))
859-
860- if len(check_dns):
861- nrpe.add_check(shortname='dns_multi',
862- description='Check DNS names are resolvable',
863- check_cmd=PLUGINS_DIR + 'check_dns_multi.sh ' + ' '.join(check_dns.split()))
864- else:
865- nrpe.remove_check(shortname='dns_multi')
866-
867- endpoint_checks = create_endpoint_checks()
868-
869- if endpoint_checks is None:
870- return False
871- else:
872- for check in endpoint_checks:
873- nrpe.add_check(**check)
874- nrpe.write()
875- return True
876-
877-
878-@when('nrpe-external-master.available')
879-def nrpe_connected(nem):
880- remove_state('os-service-checks.configured')
881-
882-
883-@when('os-service-checks.installed')
884-@when_not('os-service-checks.configured')
885-def render_config():
886- if config.get('trusted_ssl_ca', None):
887- fix_ssl()
888- creds = get_credentials()
889- if not creds:
890- hookenv.log('render_config: No credentials yet, skipping')
891- return
892- hookenv.log('render_config: Got credentials for username={}'.format(
893- creds['username']))
894- render('nagios.novarc', NOVARC, creds,
895- owner='nagios', group='nagios')
896- if render_checks():
897- hookenv.status_set('active', 'Ready')
898- set_state('os-service-checks.configured')
899- remove_state('os-service-checks.started')
900- else:
901- hookenv.status_set('blocked', 'waiting for Keystone to be ready')
902-
903-
904-@when('os-service-checks.configured')
905-@when_not('os-service-checks.started')
906-def do_restart():
907- hookenv.log('Reloading nagios-nrpe-server')
908- host.service_restart('nagios-nrpe-server')
909- hookenv.status_set('active', 'Ready')
910- set_state('os-service-checks.started')
911-
912-
913-def fix_ssl():
914- cert_file = '/usr/local/share/ca-certificates/openstack-service-checks.crt'
915- trusted_ssl_ca = config.get('trusted_ssl_ca').strip()
916- hookenv.log("Writing ssl ca cert:{}".format(trusted_ssl_ca))
917- cert_content = base64.b64decode(trusted_ssl_ca).decode()
918- with open(cert_file, 'w') as f:
919- print(cert_content, file=f)
920- subprocess.call(["/usr/sbin/update-ca-certificates"])
921-
922-
923-def create_endpoint_checks():
924- """Create an NRPE check for each Keystone catalog endpoint.
925-
926- Read the Keystone catalog, and create a check for each endpoint listed.
927- If there is a healthcheck endpoint for the API, use that URL, otherwise check
928- the url '/'.
929- If SSL, add a check for the cert.
930- """
931- # provide URLs that can be used for healthcheck for some services
932- # This also provides a nasty hack-ish way to add switches if we need
933- # for some services.
934- health_check_params = {
935- 'keystone': '/healthcheck',
936- 's3': '/healthcheck',
937- 'aodh': '/healthcheck',
938- 'cinderv3': '/v3 -e Unauthorized -d x-openstack-request-id',
939- 'cinderv2': '/v2 -e Unauthorized -d x-openstack-request-id',
940- 'cinderv1': '/v1 -e Unauthorized -d x-openstack-request-id',
941- 'ceilometer': '/ -e Unauthorized -d x-openstack-request-id',
942- 'placement': '/healthcheck -e Unauthorized -d x-openstack-request-id',
943- 'glance': '/healthcheck',
944- 'nova': '/healthcheck',
945- 'swift': config.get('swift_check_params', '/'),
946- }
947-
948- creds = get_credentials()
949- keystone_client = get_keystone_client(creds)
950- try:
951- endpoints = keystone_client.endpoints.list()
952- except keystoneauth1.exceptions.http.InternalServerError:
953- return None
954-
955- services = [x for x in keystone_client.services.list() if x.enabled]
956- nrpe_checks = []
957- for endpoint in endpoints:
958- endpoint.service_names = [x.name for x in services if x.id == endpoint.service_id]
959- service_name = endpoint.service_names[0]
960- endpoint.healthcheck_url = health_check_params.get(service_name, '/')
961- if config.get('check_{}_urls'.format(endpoint.interface)):
962- cmd_params = ['/usr/lib/nagios/plugins/check_http ']
963- check_url = urlparse(endpoint.url)
964- host_port = check_url.netloc.split(':')
965- # assume http port 80 if no port specified
966- endpoint_port = 80
967- if len(host_port) < 2:
968- if check_url.scheme == 'https':
969- endpoint_port = 443
970- else:
971- endpoint_port = host_port[1]
972- cmd_params.append('-H {} -p {}'.format(host_port[0], endpoint_port))
973- cmd_params.append('-u {}'.format(endpoint.healthcheck_url))
974- # if this is https, we want to add a check for cert expiry
975- # also need to tell check_http use use TLS
976- if check_url.scheme == 'https':
977- cmd_params.append('-S')
978- # Add an extra check for TLS cert expiry
979- cert_check = ['-C {},{}'.format(
980- config.get('tls_warn_days'),
981- config.get('tls_crit_days'))]
982- nrpe_checks.append({
983- 'shortname': "{}_{}_cert".format(
984- service_name,
985- endpoint.interface),
986- 'description': 'Certificate expiry check for {} {}'.format(
987- service_name,
988- endpoint.interface),
989- 'check_cmd': ' '.join(cmd_params + cert_check)
990- })
991- # Add the actual health check for the URL
992- nrpe_checks.append({
993- 'shortname': "{}_{}".format(
994- service_name,
995- endpoint.interface),
996- 'description': 'Endpoint url check for {} {}'.format(
997- service_name,
998- endpoint.interface),
999- 'check_cmd': (' '.join(cmd_params))})
1000- return nrpe_checks
1001-
1002-
1003-def get_keystone_client(creds):
1004- """Import the appropriate Keystone client depending on API version.
1005-
1006- Use credential info to determine the Keystone API version, and make a client session object that is to be
1007- used for authenticated communication with Keystone.
1008-
1009- :returns: a keystoneclient Client object
1010- """
1011- from keystoneclient import session
1012- if int(creds['auth_version']) >= 3:
1013- from keystoneclient.v3 import client
1014- from keystoneclient.auth.identity import v3
1015- auth_fields = ['auth_url', 'password', 'username', 'user_domain_name',
1016- 'project_domain_name', 'project_name']
1017- auth_creds = {}
1018- for key in auth_fields:
1019- auth_creds[key] = creds[key]
1020- auth = v3.Password(**auth_creds)
1021-
1022- else:
1023- from keystoneclient.v2_0 import client
1024- from keystoneclient.auth.identity import v2
1025- auth_fields = ['auth_url', 'password', 'username', 'tenant_name']
1026- auth_creds = {}
1027- for key in auth_fields:
1028- auth_creds[key] = creds[key]
1029- auth = v2.Password(**auth_creds)
1030-
1031- sess = session.Session(auth=auth)
1032- return client.Client(session=sess)
1033diff --git a/test-requirements.txt b/test-requirements.txt
1034deleted file mode 100644
1035index 77ec3f5..0000000
1036--- a/test-requirements.txt
1037+++ /dev/null
1038@@ -1,9 +0,0 @@
1039-# Lint and unit test requirements
1040-flake8>=2.2.4,<=2.4.1
1041-os-testr>=0.4.1
1042-requests>=2.18.4
1043-charms.reactive
1044-mock>=1.2
1045-nose>=1.3.7
1046-coverage>=3.6
1047-netifaces
1048diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
1049new file mode 100644
1050index 0000000..9f93ed0
1051--- /dev/null
1052+++ b/tests/functional/conftest.py
1053@@ -0,0 +1,179 @@
1054+'''
1055+Reusable pytest fixtures for functional testing
1056+
1057+Environment variables
1058+---------------------
1059+
1060+test_preserve_model:
1061+if set, the testing model won't be torn down at the end of the testing session
1062+'''
1063+
1064+import asyncio
1065+import json
1066+import os
1067+import uuid
1068+
1069+import pytest
1070+import subprocess
1071+import juju
1072+from juju.controller import Controller
1073+from juju.errors import JujuError
1074+
1075+STAT_CMD = '''python3 - <<EOF
1076+import json
1077+import os
1078+
1079+s = os.stat('%s')
1080+stat_hash = {
1081+ 'uid': s.st_uid,
1082+ 'gid': s.st_gid,
1083+ 'mode': oct(s.st_mode),
1084+ 'size': s.st_size
1085+}
1086+stat_json = json.dumps(stat_hash)
1087+print(stat_json)
1088+
1089+EOF
1090+'''
1091+
1092+
1093+@pytest.fixture(scope='module')
1094+def event_loop():
1095+ '''Override the default pytest event loop to allow for fixtures using a
1096+ broader scope'''
1097+ loop = asyncio.get_event_loop_policy().new_event_loop()
1098+ asyncio.set_event_loop(loop)
1099+ loop.set_debug(True)
1100+ yield loop
1101+ loop.close()
1102+ asyncio.set_event_loop(None)
1103+
1104+
1105+@pytest.fixture(scope='module')
1106+async def controller():
1107+ '''Connect to the current controller'''
1108+ _controller = Controller()
1109+ await _controller.connect_current()
1110+ yield _controller
1111+ await _controller.disconnect()
1112+
1113+
1114+@pytest.fixture(scope='module')
1115+async def model(controller):
1116+ '''This model lives only for the duration of the test'''
1117+ model_name = "functest-{}".format(str(uuid.uuid4())[-12:])
1118+ _model = await controller.add_model(model_name,
1119+ cloud_name=os.getenv('PYTEST_CLOUD_NAME'),
1120+ region=os.getenv('PYTEST_CLOUD_REGION'),
1121+ )
1122+ # https://github.com/juju/python-libjuju/issues/267
1123+ subprocess.check_call(['juju', 'models'])
1124+ while model_name not in await controller.list_models():
1125+ await asyncio.sleep(1)
1126+ yield _model
1127+ await _model.disconnect()
1128+ if not os.getenv('PYTEST_KEEP_MODEL'):
1129+ await controller.destroy_model(model_name)
1130+ while model_name in await controller.list_models():
1131+ await asyncio.sleep(1)
1132+
1133+
1134+@pytest.fixture
1135+async def get_app(model):
1136+ '''Returns the application requested'''
1137+ async def _get_app(name):
1138+ try:
1139+ return model.applications[name]
1140+ except KeyError:
1141+ raise JujuError("Cannot find application {}".format(name))
1142+ return _get_app
1143+
1144+
1145+@pytest.fixture
1146+async def get_unit(model):
1147+ '''Returns the requested <app_name>/<unit_number> unit'''
1148+ async def _get_unit(name):
1149+ try:
1150+ (app_name, unit_number) = name.split('/')
1151+ return model.applications[app_name].units[unit_number]
1152+ except (KeyError, ValueError):
1153+ raise JujuError("Cannot find unit {}".format(name))
1154+ return _get_unit
1155+
1156+
1157+@pytest.fixture
1158+async def get_entity(get_unit, get_app):
1159+ '''Returns a unit or an application'''
1160+ async def _get_entity(name):
1161+ try:
1162+ return await get_unit(name)
1163+ except JujuError:
1164+ try:
1165+ return await get_app(name)
1166+ except JujuError:
1167+ raise JujuError("Cannot find entity {}".format(name))
1168+ return _get_entity
1169+
1170+
1171+@pytest.fixture
1172+async def run_command(get_unit):
1173+ '''
1174+ Runs a command on a unit.
1175+
1176+ :param cmd: Command to be run
1177+ :param target: Unit object or unit name string
1178+ '''
1179+ async def _run_command(cmd, target):
1180+ unit = (
1181+ target
1182+ if isinstance(target, juju.unit.Unit)
1183+ else await get_unit(target)
1184+ )
1185+ action = await unit.run(cmd)
1186+ return action.results
1187+ return _run_command
1188+
1189+
1190+@pytest.fixture
1191+async def file_stat(run_command):
1192+ '''
1193+ Runs stat on a file
1194+
1195+ :param path: File path
1196+ :param target: Unit object or unit name string
1197+ '''
1198+ async def _file_stat(path, target):
1199+ cmd = STAT_CMD % path
1200+ results = await run_command(cmd, target)
1201+ return json.loads(results['Stdout'])
1202+ return _file_stat
1203+
1204+
1205+@pytest.fixture
1206+async def file_contents(run_command):
1207+ '''
1208+ Returns the contents of a file
1209+
1210+ :param path: File path
1211+ :param target: Unit object or unit name string
1212+ '''
1213+ async def _file_contents(path, target):
1214+ cmd = 'cat {}'.format(path)
1215+ results = await run_command(cmd, target)
1216+ return results['Stdout']
1217+ return _file_contents
1218+
1219+
1220+@pytest.fixture
1221+async def reconfigure_app(get_app, model):
1222+ '''Applies a different config to the requested app'''
1223+ async def _reconfigure_app(cfg, target):
1224+ application = (
1225+ target
1226+ if isinstance(target, juju.application.Application)
1227+ else await get_app(target)
1228+ )
1229+ await application.set_config(cfg)
1230+ await application.get_config()
1231+ await model.block_until(lambda: application.status == 'active')
1232+ return _reconfigure_app
1233diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt
1234new file mode 100644
1235index 0000000..f76bfbb
1236--- /dev/null
1237+++ b/tests/functional/requirements.txt
1238@@ -0,0 +1,6 @@
1239+flake8
1240+juju
1241+mock
1242+pytest
1243+pytest-asyncio
1244+requests
1245diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py
1246new file mode 100644
1247index 0000000..add1c63
1248--- /dev/null
1249+++ b/tests/functional/test_deploy.py
1250@@ -0,0 +1,92 @@
1251+import collections
1252+import os
1253+
1254+import pytest
1255+
1256+# Treat all tests as coroutines
1257+pytestmark = pytest.mark.asyncio
1258+
1259+SERIES = ['xenial', 'bionic']
1260+CHARM_BUILD_DIR = os.getenv('CHARM_BUILD_DIR', '.').rstrip('/')
1261+
1262+
1263+# Custom fixtures
1264+
1265+@pytest.fixture(scope='module', params=SERIES)
1266+async def osc_apps(request, model):
1267+ series = request.param
1268+ app = model.applications.get('openstack-service-checks-{}'.format(series))
1269+ return app
1270+
1271+
1272+@pytest.fixture(scope='module', params=SERIES)
1273+async def units(request, apps):
1274+ return apps.units
1275+
1276+
1277+def app_names(series):
1278+ apps_list = ('keystone neutron-api nova-cloud-controller percona-cluster rabbitmq-server'
1279+ ' openstack-service-checks nagios nrpe').split()
1280+ apps = dict([(app, '{}-{}'.format(app, series)) for app in apps_list])
1281+ return apps
1282+
1283+
1284+@pytest.fixture(scope='module', params=SERIES)
1285+async def deploy_openstack(request, model):
1286+ series = request.param
1287+ nunits = collections.defaultdict(lambda: 1)
1288+ apps = app_names(series)
1289+ active_apps = []
1290+ for app in apps:
1291+ if app == 'openstack-service-checks':
1292+ continue
1293+ if app == 'nrpe':
1294+ nunits[app] = 0
1295+ app_deploy = await model.deploy('cs:{}'.format(app), series=series,
1296+ application_name=apps[app], num_units=nunits[app])
1297+ if app not in ('nova-cloud-controller', 'nrpe'):
1298+ active_apps.append(app_deploy)
1299+
1300+ await model.add_relation('{}:shared-db'.format(apps['keystone']),
1301+ '{}:shared-db'.format(apps['percona-cluster']))
1302+ await model.add_relation('{}:monitors'.format(apps['nrpe']),
1303+ '{}:monitors'.format(apps['nagios']))
1304+
1305+ for app in 'neutron-api nova-cloud-controller'.split():
1306+ if app == 'openstack-service-checks':
1307+ continue
1308+
1309+ await model.add_relation('{}:amqp'.format(apps[app]),
1310+ '{}:amqp'.format(apps['rabbitmq-server']))
1311+ await model.add_relation('{}:shared-db'.format(apps[app]),
1312+ '{}:shared-db'.format(apps['percona-cluster']))
1313+ await model.add_relation('{}:identity-service'.format(apps[app]),
1314+ '{}:identity-service'.format(apps['keystone']))
1315+
1316+ yield active_apps
1317+
1318+
1319+@pytest.fixture(scope='module', params=SERIES)
1320+async def deploy_app(request, model):
1321+ series = request.param
1322+ apps = app_names(series)
1323+
1324+ # Starts a deploy for each series
1325+ osc_app = await model.deploy(os.path.join(CHARM_BUILD_DIR, 'openstack-service-checks'),
1326+ series=series, application_name=apps['openstack-service-checks'])
1327+
1328+ for app in 'keystone nrpe'.split():
1329+ await model.add_relation(apps[app], apps['openstack-service-checks'])
1330+
1331+ yield osc_app
1332+
1333+
1334+# Tests
1335+
1336+async def test_openstackservicechecks_deploy_openstack(deploy_openstack, model):
1337+ await model.block_until(lambda: all([app.status == 'active' for app in deploy_openstack]),
1338+ timeout=900)
1339+
1340+
1341+async def test_openstackservicechecks_deploy(deploy_app, model):
1342+ await model.block_until(lambda: deploy_app.status == 'active', timeout=600)
1343diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
1344new file mode 100644
1345index 0000000..32a4af9
1346--- /dev/null
1347+++ b/tests/unit/conftest.py
1348@@ -0,0 +1,77 @@
1349+import unittest.mock as mock
1350+
1351+import pytest
1352+
1353+
1354+# If layer options are used, add this to openstackservicechecks
1355+# and import layer in lib_openstack_service_checks
1356+@pytest.fixture
1357+def mock_layers(monkeypatch):
1358+ import sys
1359+ sys.modules['charms.layer'] = mock.Mock()
1360+ sys.modules['reactive'] = mock.Mock()
1361+ # Mock any functions in layers that need to be mocked here
1362+
1363+ def options(layer):
1364+ # mock options for layers here
1365+ if layer == 'example-layer':
1366+ options = {'port': 9999}
1367+ return options
1368+ else:
1369+ return None
1370+
1371+ monkeypatch.setattr('lib_openstack_service_checks.layer.options', options)
1372+
1373+
1374+@pytest.fixture
1375+def mock_hookenv_config(monkeypatch):
1376+ import yaml
1377+
1378+ def mock_config():
1379+ cfg = {}
1380+ yml = yaml.safe_load(open('./config.yaml'))
1381+
1382+ # Load all defaults
1383+ for key, value in yml['options'].items():
1384+ cfg[key] = value['default']
1385+
1386+ # Manually add cfg from other layers
1387+ # cfg['my-other-layer'] = 'mock'
1388+ return cfg
1389+
1390+ monkeypatch.setattr('lib_openstack_service_checks.hookenv.config', mock_config)
1391+
1392+
1393+@pytest.fixture
1394+def mock_remote_unit(monkeypatch):
1395+ monkeypatch.setattr('lib_openstack_service_checks.hookenv.remote_unit', lambda: 'unit-mock/0')
1396+
1397+
1398+@pytest.fixture
1399+def mock_charm_dir(monkeypatch):
1400+ monkeypatch.setattr('lib_openstack_service_checks.hookenv.charm_dir', lambda: '/mock/charm/dir')
1401+
1402+
1403+@pytest.fixture
1404+def mock_unitdata_keystonecreds(monkeypatch):
1405+ creds = {'keystonecreds': {'username': 'nagios',
1406+ 'password': 'password',
1407+ 'project_name': 'services',
1408+ 'tenant_name': 'services',
1409+ 'user_domain_name': 'service_domain',
1410+ 'project_domain_name': 'service_domain',
1411+ }
1412+ }
1413+ monkeypatch.setattr('lib_openstack_service_checks.unitdata.kv', lambda: creds)
1414+
1415+
1416+@pytest.fixture
1417+def openstackservicechecks(tmpdir, mock_hookenv_config, mock_charm_dir, monkeypatch):
1418+ from lib_openstack_service_checks import OSCHelper
1419+ helper = OSCHelper()
1420+
1421+ # Any other functions that load helper will get this version
1422+ monkeypatch.setattr('lib_openstack_service_checks.hookenv.log', lambda msg, level='INFO': None)
1423+ monkeypatch.setattr('lib_openstack_service_checks.OSCHelper', lambda: helper)
1424+
1425+ return helper
1426diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
1427new file mode 100644
1428index 0000000..1c828b8
1429--- /dev/null
1430+++ b/tests/unit/requirements.txt
1431@@ -0,0 +1,10 @@
1432+charmhelpers
1433+charms.reactive
1434+mock
1435+pytest
1436+pytest-cov
1437+os-testr>=0.4.1
1438+requests>=2.18.4
1439+netifaces
1440+keystoneauth1
1441+python-keystoneclient
1442diff --git a/tests/unit/test_lib.py b/tests/unit/test_lib.py
1443new file mode 100644
1444index 0000000..d10d44a
1445--- /dev/null
1446+++ b/tests/unit/test_lib.py
1447@@ -0,0 +1,44 @@
1448+from pytest import mark
1449+
1450+
1451+def test_openstackservicechecks_common_properties(openstackservicechecks):
1452+ '''Verify the most common properties from the class or default config.yaml'''
1453+ assert isinstance(openstackservicechecks.charm_config, dict)
1454+ assert openstackservicechecks.novarc == '/var/lib/nagios/nagios.novarc'
1455+ assert openstackservicechecks.plugins_dir == '/usr/local/lib/nagios/plugins/'
1456+ assert openstackservicechecks.nova_warn == 2
1457+ assert openstackservicechecks.nova_crit == 1
1458+ assert openstackservicechecks.skip_disabled == ''
1459+ assert openstackservicechecks.check_dns == ''
1460+
1461+
1462+def test_openstackservicechecks_get_keystone_credentials_unitdata(
1463+ openstackservicechecks, mock_unitdata_keystonecreds):
1464+ """Checks the expected behavior when 'os-credentials' are not shared, but the application is related to keystone.
1465+ """
1466+ assert openstackservicechecks.get_keystone_credentials() == {
1467+ 'username': 'nagios', 'password': 'password', 'project_name': 'services',
1468+ 'tenant_name': 'services', 'user_domain_name': 'service_domain',
1469+ 'project_domain_name': 'service_domain'
1470+ }
1471+
1472+
1473+@mark.parametrize('os_credentials,expected', [
1474+ (('username=nagios, password=password, region_name=RegionOne, auth_url="http://XX.XX.XX.XX:5000/v3",'
1475+ 'credentials_project=services, domain=service_domain'),
1476+ {'username': 'nagios', 'password': 'password', 'project_name': 'services', 'auth_version': 3,
1477+ 'user_domain_name': 'service_domain', 'project_domain_name': 'service_domain', 'region_name': 'RegionOne',
1478+ 'auth_url': 'http://XX.XX.XX.XX:5000/v3'},
1479+ ),
1480+ (('username=nagios, password=password, region_name=RegionOne, auth_url="http://XX.XX.XX.XX:5000/v2.0",'
1481+ 'credentials_project=services'),
1482+ {'username': 'nagios', 'password': 'password', 'tenant_name': 'services', 'region_name': 'RegionOne',
1483+ 'auth_url': 'http://XX.XX.XX.XX:5000/v2.0'},
1484+ )
1485+])
1486+def test_openstackservicechecks_get_keystone_credentials_oscredentials(
1487+ os_credentials, expected, openstackservicechecks, mock_unitdata_keystonecreds):
1488+ """Checks the expected behavior when keystone v2 and v3 data is shared via the 'os-credentials' config parameter.
1489+ """
1490+ openstackservicechecks.charm_config['os-credentials'] = os_credentials
1491+ assert openstackservicechecks.get_os_credentials() == expected
1492diff --git a/tox.ini b/tox.ini
1493index f4560b5..3a54b37 100644
1494--- a/tox.ini
1495+++ b/tox.ini
1496@@ -1,40 +1,45 @@
1497 [tox]
1498 skipsdist=True
1499-envlist = pep8
1500+envlist = unit, functional
1501 skip_missing_interpreters = True
1502
1503 [testenv]
1504-setenv = VIRTUAL_ENV={envdir}
1505- PYTHONHASHSEED=0
1506- TERM=linux
1507- LAYER_PATH={toxinidir}/layers
1508- INTERFACE_PATH={toxinidir}/interfaces
1509- JUJU_REPOSITORY={toxinidir}/build
1510-passenv = http_proxy https_proxy
1511-install_command =
1512- pip install {opts} {packages}
1513-deps =
1514- -r{toxinidir}/requirements.txt
1515-
1516-[testenv:build]
1517 basepython = python3
1518-commands =
1519- charm-build --log-level DEBUG -o {toxinidir}/build src {posargs}
1520+setenv =
1521+ PYTHONPATH = .
1522
1523-[testenv:py3]
1524-basepython = python3
1525-deps = -r{toxinidir}/test-requirements.txt
1526-commands = ostestr {posargs}
1527+[testenv:unit]
1528+commands = pytest -v --ignore {toxinidir}/tests/functional \
1529+ --cov=lib \
1530+ --cov=reactive \
1531+ --cov=actions \
1532+ --cov-report=term \
1533+ --cov-report=annotate:reports/annotated \
1534+ --cov-report=html:reports/html
1535+deps = -r{toxinidir}/tests/unit/requirements.txt
1536+ -r{toxinidir}/requirements.txt
1537+setenv = PYTHONPATH={toxinidir}/lib
1538
1539-[testenv:pep8]
1540-basepython = python3
1541-deps = -r{toxinidir}/test-requirements.txt
1542-commands = flake8 {posargs} --max-complexity=10 --max-line-length=120 reactive files unit_tests
1543+[testenv:functional]
1544+passenv =
1545+ HOME
1546+ CHARM_BUILD_DIR
1547+ PATH
1548+ PYTEST_KEEP_MODEL
1549+ PYTEST_CLOUD_NAME
1550+ PYTEST_CLOUD_REGION
1551+commands = pytest -v --ignore {toxinidir}/tests/unit
1552+deps = -r{toxinidir}/tests/functional/requirements.txt
1553+ -r{toxinidir}/requirements.txt
1554
1555-[testenv:venv]
1556-basepython = python3
1557-commands = {posargs}
1558+[testenv:lint]
1559+commands = flake8
1560+deps = flake8
1561
1562 [flake8]
1563-# E402 ignore necessary for path append before sys module import in actions
1564-ignore = E402
1565+exclude =
1566+ .git,
1567+ __pycache__,
1568+ .tox,
1569+max-line-length = 120
1570+max-complexity = 10

Subscribers

People subscribed via source and target branches