Merge ~aluria/charm-openstack-service-checks/+git/charm-openstack-service-checks:bug/1821316-rewrite into ~canonical-bootstack/charm-openstack-service-checks:master
- Git
- lp:~aluria/charm-openstack-service-checks/+git/charm-openstack-service-checks
- bug/1821316-rewrite
- Merge into master
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) |
||||
Related bugs: |
|
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-
* separation between charm logic (reactive) and helper functions (lib)
* improvement on charm states and messages
* unit and functional tests added (using pytest framework)
Description of the change
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
🤖 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.
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/
Once both conflicts are resolved, run "git merge --continue" and that should be it.
1. https:/
2. https:/
3. https:/
Alvaro Uria (aluria) wrote : | # |
The purpose of this MP is to:
1) move all code off "src/" folder, per latest template-
2) update tox.ini and Makefile with new environment variables (PYTEST_KEEP_MODEL vs old TEST_PRESERVE_
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.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change has no commit message, setting status to needs review.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 1c0564e386ee87e
Preview Diff
1 | diff --git a/.gitignore b/.gitignore |
2 | index 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/* |
38 | diff --git a/Makefile b/Makefile |
39 | index 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 |
116 | diff --git a/config.yaml b/config.yaml |
117 | index 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 |
128 | diff --git a/files/plugins/check_nova_services.py b/files/plugins/check_nova_services.py |
129 | index 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) |
142 | diff --git a/interfaces/.empty b/interfaces/.empty |
143 | new file mode 100644 |
144 | index 0000000..792d600 |
145 | --- /dev/null |
146 | +++ b/interfaces/.empty |
147 | @@ -0,0 +1 @@ |
148 | +# |
149 | diff --git a/layer.yaml b/layer.yaml |
150 | index 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' |
162 | diff --git a/layers/.empty b/layers/.empty |
163 | new file mode 100644 |
164 | index 0000000..792d600 |
165 | --- /dev/null |
166 | +++ b/layers/.empty |
167 | @@ -0,0 +1 @@ |
168 | +# |
169 | diff --git a/lib/lib_openstack_service_checks.py b/lib/lib_openstack_service_checks.py |
170 | new file mode 100644 |
171 | index 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) |
422 | diff --git a/metadata.yaml b/metadata.yaml |
423 | index 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 |
447 | diff --git a/reactive/openstack_service_checks.py b/reactive/openstack_service_checks.py |
448 | new file mode 100644 |
449 | index 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() |
701 | diff --git a/reactive/service_checks.py b/reactive/service_checks.py |
702 | deleted file mode 100644 |
703 | index 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) |
1033 | diff --git a/test-requirements.txt b/test-requirements.txt |
1034 | deleted file mode 100644 |
1035 | index 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 |
1048 | diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py |
1049 | new file mode 100644 |
1050 | index 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 |
1233 | diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt |
1234 | new file mode 100644 |
1235 | index 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 |
1245 | diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py |
1246 | new file mode 100644 |
1247 | index 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) |
1343 | diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py |
1344 | new file mode 100644 |
1345 | index 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 |
1426 | diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt |
1427 | new file mode 100644 |
1428 | index 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 |
1442 | diff --git a/tests/unit/test_lib.py b/tests/unit/test_lib.py |
1443 | new file mode 100644 |
1444 | index 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 |
1492 | diff --git a/tox.ini b/tox.ini |
1493 | index 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 |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.