From 47f80e0ed6f6ad2dd01bea07b767c5405dac6d17 Mon Sep 17 00:00:00 2001 From: Nicolas Pochet Date: Tue, 4 Jun 2019 15:27:20 +0200 Subject: [PATCH] Add contrail-service-checks charm * Add reactive contrail-service-checks charm * Add unit and functional tests --- contrail-service-checks/LICENSE | 202 +++++++++++++ contrail-service-checks/README.md | 29 ++ contrail-service-checks/config.yaml | 35 +++ .../files/plugins/check_contrail_alarms.py | 70 +++++ contrail-service-checks/icon.svg | 279 ++++++++++++++++++ contrail-service-checks/layer.yaml | 14 + .../lib/lib_contrail_service_checks.py | 87 ++++++ contrail-service-checks/metadata.yaml | 25 ++ .../reactive/contrail_service_checks.py | 186 ++++++++++++ .../templates/keystone.yaml | 6 + contrail-service-checks/test-requirements.txt | 11 + .../tests/bundles/bionic-queens.yaml | 19 ++ .../overlays/local-charm-overlay.yaml.j2 | 3 + contrail-service-checks/tests/tests.yaml | 11 + contrail-service-checks/tox.ini | 46 +++ .../unit_tests/test_check_contrail_alarms.py | 86 ++++++ .../test_contrail_service_checks.py | 188 ++++++++++++ .../test_lib_contrail_service_checks.py | 155 ++++++++++ 18 files changed, 1452 insertions(+) create mode 100644 contrail-service-checks/LICENSE create mode 100644 contrail-service-checks/README.md create mode 100644 contrail-service-checks/config.yaml create mode 100755 contrail-service-checks/files/plugins/check_contrail_alarms.py create mode 100644 contrail-service-checks/icon.svg create mode 100644 contrail-service-checks/layer.yaml create mode 100644 contrail-service-checks/lib/lib_contrail_service_checks.py create mode 100644 contrail-service-checks/metadata.yaml create mode 100644 contrail-service-checks/reactive/contrail_service_checks.py create mode 100644 contrail-service-checks/templates/keystone.yaml create mode 100644 contrail-service-checks/test-requirements.txt create mode 100644 contrail-service-checks/tests/bundles/bionic-queens.yaml create mode 100644 contrail-service-checks/tests/bundles/overlays/local-charm-overlay.yaml.j2 create mode 100644 contrail-service-checks/tests/tests.yaml create mode 100644 contrail-service-checks/tox.ini create mode 100644 contrail-service-checks/unit_tests/test_check_contrail_alarms.py create mode 100644 contrail-service-checks/unit_tests/test_contrail_service_checks.py create mode 100644 contrail-service-checks/unit_tests/test_lib_contrail_service_checks.py diff --git a/contrail-service-checks/LICENSE b/contrail-service-checks/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/contrail-service-checks/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/contrail-service-checks/README.md b/contrail-service-checks/README.md new file mode 100644 index 0000000..cd512d8 --- /dev/null +++ b/contrail-service-checks/README.md @@ -0,0 +1,29 @@ +# Overview + +The goal of this charm is to provide NRPE checks for Contrail. It will pull the alarms from Contrail analytics. If an alarm is found, a CRITICAL response is sent to NRPE. If no alarm is found, an OK response is sent to NRPE. + +# Usage + +```bash +# Assuming that a working Contrail + OpenStack environment is already deployed with Nagios and NRPE +charm build ./contrail-service-checks +juju deploy /tmp/charm-builds/contrail-service-checks +juju config contrail-service-checks contrail_analytics_vip=VIP +juju add-relation nrpe contrail-service-checks +juju add-relation keystone contrail-service-checks +``` + + +## Scale out Usage + +There is no use to scale this charm. + +## Known Limitations and Issues + +* No scale out +* Not tested against a cloud with HTTPS for Keystone +* Basic checks of the Contrail alarms: + * If there's any alarm -> CRITICAL + * If there's no alarm -> OK + * In any case, print the result of the query + diff --git a/contrail-service-checks/config.yaml b/contrail-service-checks/config.yaml new file mode 100644 index 0000000..de4c76a --- /dev/null +++ b/contrail-service-checks/config.yaml @@ -0,0 +1,35 @@ +options: + os-credentials: + default: "" + type: string + description: | + Comma separated OpenStack credentials to be used by nagios. + It is strongly recommended this be a user with a dedicated role, + and not a full admin. Takes the format of + username=foo, password=bar, project_name=baz, region_name=Region1, auth_url=http://127.0.0.1:3535 + nagios_context: + default: "juju" + type: string + description: | + Used by the nrpe subordinate charms. + A string that will be prepended to instance name to set the host name + in nagios. So for instance the hostname would be something like: + juju-myservice-0 + If you're running multiple environments with the same services in them + this allows you to differentiate between them. + nagios_servicegroups: + default: "" + type: string + description: | + A comma-separated list of nagios servicegroups. + If left empty, the nagios_context will be used as the servicegroup + trusted_ssl_ca: + type: string + default: '' + description: | + base64 encoded SSL ca cert to use for OpenStack API client connections. + contrail_analytics_vip: + type: string + default: '' + description: The VIP used for Contrail Analytics + diff --git a/contrail-service-checks/files/plugins/check_contrail_alarms.py b/contrail-service-checks/files/plugins/check_contrail_alarms.py new file mode 100755 index 0000000..a9953ff --- /dev/null +++ b/contrail-service-checks/files/plugins/check_contrail_alarms.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +from keystoneclient.v3 import client +import argparse +import requests +import sys +import yaml + +NAGIOS_OK = 0 +NAGIOS_WARNING = 1 +NAGIOS_CRITICAL = 2 + + +def check_contrail_alarms(vip, token): + """ + Check the alarms in Contrail. + @param str vip: VIP of Contrail + @param str token: Token for the authentication + @returns: None + """ + url = 'http://{}:8081/analytics/alarms'.format(vip) + headers = { + 'X-Auth-Token': token + } + r = requests.get(url=url, headers=headers) + result = r.json() + print(result) + if result: + return NAGIOS_CRITICAL + return NAGIOS_OK + + +def get_auth_token(auth_url, user, password, project, domain): + """ + Retrieve an OpenStack token to use to authenticate against Contrail + @param str auth_url: Keystone authentication URL + @param str user: OpenStack username + @param str password: OpenStack password + @param str project: OpenStack project + @param str domain: OpenStack domain + @returns: str. The token or None + """ + keystone = client.Client( + auth_url=auth_url, + username=user, + password=password, + project_name=project, + user_domain_name=domain, + project_domain_name=domain + ) + return keystone.auth_ref.get('auth_token') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Check Contrail alarms') + parser.add_argument('--env', dest='env', + default='/var/lib/nagios/keystone.yaml', + help="Credentials file to use for this check") + args = parser.parse_args() + env_file = args.env + with open(env_file) as f: + cloud = yaml.safe_load(f) + auth_url = cloud.get('auth_url') + user = cloud.get('user') + password = cloud.get('password') + project = cloud.get('project') + domain = cloud.get('domain') + vip = cloud.get('contrail_analytics_vip') + token = get_auth_token(auth_url, user, password, project, domain) + sys.exit(check_contrail_alarms(vip, token)) diff --git a/contrail-service-checks/icon.svg b/contrail-service-checks/icon.svg new file mode 100644 index 0000000..96a5d0c --- /dev/null +++ b/contrail-service-checks/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/contrail-service-checks/layer.yaml b/contrail-service-checks/layer.yaml new file mode 100644 index 0000000..f51a346 --- /dev/null +++ b/contrail-service-checks/layer.yaml @@ -0,0 +1,14 @@ +includes: + - 'layer:basic' + - 'interface:keystone-credentials' + - 'interface:nrpe-external-master' +options: + basic: + use_venv: true + packages: + - nagios-nrpe-server + - python3-keystoneclient + - python3-openstackclient + - python3-requests + - python3-yaml +repo: https://github.com/n-pochet/contrail-service-checks diff --git a/contrail-service-checks/lib/lib_contrail_service_checks.py b/contrail-service-checks/lib/lib_contrail_service_checks.py new file mode 100644 index 0000000..d841a50 --- /dev/null +++ b/contrail-service-checks/lib/lib_contrail_service_checks.py @@ -0,0 +1,87 @@ +import os + +from charmhelpers.core.templating import render +from charmhelpers.contrib.openstack.utils import config_flags_parser +from charmhelpers.core import hookenv, host, unitdata +from charmhelpers.contrib.charmsupport.nrpe import NRPE + + +class CSCCredentialsError(Exception): + pass + + +class CSCHelper(): + def __init__(self): + self._charm_config = None + + def store_keystone_credentials(self, creds): + '''store keystone credentials''' + unitdata.kv().set('keystonecreds', creds) + + @property + def charm_config(self): + if self._charm_config is None: + self._charm_config = hookenv.config() + return self._charm_config + + @property + def oscreds(self): + return '/var/lib/nagios/keystone.yaml' + + @property + def plugins_dir(self): + return '/usr/local/lib/nagios/plugins/' + + def get_os_credentials(self): + ident_creds = config_flags_parser(self.charm_config['os-credentials']) + + # Check that auth_url is in the credentials from the config + if not ident_creds.get('auth_url'): + raise CSCCredentialsError('auth_url') + creds = {} + all_attrs = ('username password region_name auth_url' + ' project_name domain').split() + missing = [k for k in all_attrs if k not in ident_creds] + # Check that there's no missing mandatory parameter + if missing: + raise CSCCredentialsError(', '.join(missing)) + + # Strip the auth_url as it might contain quotes + ident_creds['auth_url'] = ident_creds['auth_url'].strip('\"\'') + creds.update(dict([(k, ident_creds.get(k)) for k in all_attrs])) + + return creds + + def get_keystone_credentials(self): + '''retrieve keystone credentials from either config or relation data + + If config 'os-crendentials' is set, return that info, + otherwise look for a keystonecreds relation data' + + :return: dict of credential information for keystone + ''' + return unitdata.kv().get('keystonecreds') + + def render_checks(self, creds): + render(source='keystone.yaml', target=self.oscreds, context=creds, + owner='nagios', group='nagios') + + nrpe = NRPE() + if not os.path.exists(self.plugins_dir): + os.makedirs(self.plugins_dir) + + charm_plugin_dir = os.path.join(hookenv.charm_dir(), + 'files', + 'plugins/') + host.rsync(charm_plugin_dir, + self.plugins_dir, + options=['--executability']) + + contrail_check_command = os.path.join(self.plugins_dir, + 'check_contrail_alarms.py') + nrpe.add_check(shortname='contrail_alarms', + description='Check Contrail alarms', + check_cmd=contrail_check_command, + ) + + nrpe.write() diff --git a/contrail-service-checks/metadata.yaml b/contrail-service-checks/metadata.yaml new file mode 100644 index 0000000..59479f8 --- /dev/null +++ b/contrail-service-checks/metadata.yaml @@ -0,0 +1,25 @@ +name: contrail-service-checks +summary: This charm provides Contrail service checks for Nagios +maintainer: Andrey Pavlov +description: | + This charm provides Contrail service checks for Nagios +tags: + - contrail + - openstack + - monitoring +subordinate: false +series: +- bionic +provides: + nrpe-external-master: + interface: nrpe-external-master + scope: container + optional: true +requires: + identity-credentials: + interface: keystone-credentials + optional: true +extra-bindings: + public: + admin: + internal: diff --git a/contrail-service-checks/reactive/contrail_service_checks.py b/contrail-service-checks/reactive/contrail_service_checks.py new file mode 100644 index 0000000..c9a83a3 --- /dev/null +++ b/contrail-service-checks/reactive/contrail_service_checks.py @@ -0,0 +1,186 @@ +import base64 +import subprocess + +from charmhelpers.core import hookenv, host, unitdata +from charms.reactive import clear_flag, set_flag, when, when_not + +from lib_contrail_service_checks import ( + CSCHelper, + CSCCredentialsError +) + +CERT_FILE = '/usr/local/share/ca-certificates/openstack-service-checks.crt' + + +helper = CSCHelper() + + +@when('config.changed') +def config_changed(): + clear_flag('contrail-service-checks.configured') + + +@when_not('contrail-service-checks.installed') +@when('nrpe-external-master.available') +def install_contrail_service_checks(): + """Entry point to start configuring the unit + + Triggered if related to the nrpe-external-master relation. + Some relation data can be initialized if the application is related to + keystone. + """ + set_flag('contrail-service-checks.installed') + clear_flag('contrail-service-checks.configured') + + +@when_not('identity-credentials.available') +@when('identity-credentials.connected') +def configure_ident_username(keystone): + """Requests a user to the Identity Service + """ + username = 'nagios-contrail' + keystone.request_credentials(username) + clear_flag('contrail-service-checks.stored-creds') + + +@when_not('contrail-service-checks.stored-creds') +@when('identity-credentials.available') +def save_creds(keystone): + """Collect and save credentials from Keystone relation. + + Get credentials from the Keystone relation, + reformat them into something the Keystone client can use, and + save them into the unitdata. + """ + creds = {'username': keystone.credentials_username(), + 'password': keystone.credentials_password(), + 'region': keystone.region(), + } + api_url = 'v3' + try: + domain = keystone.domain() + except AttributeError: + domain = 'service_domain' + creds.update({'project_name': keystone.credentials_project(), + 'auth_version': '3', + 'domain': domain}) + + creds['auth_url'] = '{proto}://{host}:{port}/{api_url}'.format( + proto=keystone.auth_protocol(), host=keystone.auth_host(), + port=keystone.auth_port(), api_url=api_url) + + helper.store_keystone_credentials(creds) + set_flag('contrail-service-checks.stored-creds') + clear_flag('contrail-service-checks.configured') + + +@when_not('identity-credentials.connected') +@when_not('identity-credentials.available') +@when('contrail-service-checks.stored-creds') +def allow_keystone_store_overwrite(): + """Allow unitdata overwrite if keystone relation is recycled. + """ + clear_flag('contrail-service-checks.stored-creds') + + +def get_credentials(): + """Get credential info from either config or relation data + + If config 'os-credentials' is set, return it. Otherwise look for a + keystonecreds relation data. + """ + try: + creds = helper.get_os_credentials() + except CSCCredentialsError as error: + creds = helper.get_keystone_credentials() + if not creds: + hookenv.log('render_config: No credentials yet, skipping') + hookenv.status_set('blocked', + 'Missing os-credentials vars: {}'.format(error)) + return + contrail_vip = helper.charm_config['contrail_analytics_vip'] + creds['contrail_analytics_vip'] = contrail_vip + return creds + + +@when('contrail-service-checks.installed') +@when_not('contrail-service-checks.configured') +def render_config(): + """Render nrpe checks from the templates + + This code is only triggered after the nrpe relation is set. If a relation + with keystone is later set, it will be re-triggered. On the other hand, + if a keystone relation exists but not a nrpe relation, it won't be run. + + Furthermore, juju config os-credentials take precedence over keystone + related data. + """ + def block_tls_failure(error): + hookenv.log('update-ca-certificates failed: {}'.format(error), + hookenv.ERROR) + hookenv.status_set('blocked', + 'update-ca-certificates error. check logs') + return + + creds = get_credentials() + if not creds: + return + + # Fix TLS + if helper.charm_config['trusted_ssl_ca'].strip(): + trusted_ssl_ca = helper.charm_config['trusted_ssl_ca'].strip() + hookenv.log('Writing ssl ca cert:{}'.format(trusted_ssl_ca)) + cert_content = base64.b64decode(trusted_ssl_ca).decode() + try: + with open(CERT_FILE, 'w') as fd: + fd.write(cert_content) + subprocess.call(['/usr/sbin/update-ca-certificates']) + + except subprocess.CalledProcessError as error: + block_tls_failure(error) + return + except PermissionError as error: + block_tls_failure(error) + return + + hookenv.log('render_config: Got credentials for' + ' username={}'.format(creds.get('username'))) + + helper.render_checks(creds) + + set_flag('contrail-service-checks.configured') + clear_flag('contrail-service-checks.started') + + +@when('contrail-service-checks.configured') +@when_not('contrail-service-checks.started') +def do_restart(): + hookenv.log('Reloading nagios-nrpe-server') + host.service_restart('nagios-nrpe-server') + hookenv.status_set('active', 'Unit is ready') + set_flag('contrail-service-checks.started') + + +@when('config.changed.os-credentials') +@when('nrpe-external-master.available') +def do_reconfigure_nrpe(): + clear_flag('contrail-service-checks.configured') + + +@when_not('nrpe-external-master.available') +def missing_nrpe(): + """Avoid a user action to be missed or overwritten by another hook + """ + if hookenv.hook_name() != 'update-status': + hookenv.status_set('blocked', 'Missing relations: nrpe') + + +@when('contrail-service-checks.installed') +@when('nrpe-external-master.available') +def parse_hooks(): + if hookenv.hook_name() == 'upgrade-charm': + kv = unitdata.kv() + creds = kv.get('keystonecreds') + kv.set('keystonecreds', creds) + # render configs again + do_reconfigure_nrpe() diff --git a/contrail-service-checks/templates/keystone.yaml b/contrail-service-checks/templates/keystone.yaml new file mode 100644 index 0000000..ae9fd8f --- /dev/null +++ b/contrail-service-checks/templates/keystone.yaml @@ -0,0 +1,6 @@ +user: {{ username }} +project: {{ project_name }} +password: {{ password }} +auth_url: {{ auth_url }} +domain: {{ domain }} +contrail_analytics_vip: {{ contrail_analytics_vip }} \ No newline at end of file diff --git a/contrail-service-checks/test-requirements.txt b/contrail-service-checks/test-requirements.txt new file mode 100644 index 0000000..8152ba5 --- /dev/null +++ b/contrail-service-checks/test-requirements.txt @@ -0,0 +1,11 @@ +flake8 +pytest +pytest-cov +pytest-mock +charmhelpers +charms.reactive +requests +requests-mock +fixture +charm-tools>=2.4.4 +git+https://github.com/openstack-charmers/zaza.git#egg=zaza \ No newline at end of file diff --git a/contrail-service-checks/tests/bundles/bionic-queens.yaml b/contrail-service-checks/tests/bundles/bionic-queens.yaml new file mode 100644 index 0000000..761cedb --- /dev/null +++ b/contrail-service-checks/tests/bundles/bionic-queens.yaml @@ -0,0 +1,19 @@ +series: bionic +relations: + - [nagios:monitors, nrpe:monitors] + - [nrpe:nrpe-external-master, contrail-service-checks:nrpe-external-master] +applications: + nagios: + charm: cs:nagios + num_units: 1 + series: bionic + nrpe: + charm: cs:nrpe + series: bionic + contrail-service-checks: + charm: contrail-service-checks + num_units: 1 + options: + contrail_analytics_vip: VIP + os-credentials: username=foo, password=bar, project_name=baz, region_name=Region1, auth_url=http://127.0.0.1:3535, domain=domain + series: bionic \ No newline at end of file diff --git a/contrail-service-checks/tests/bundles/overlays/local-charm-overlay.yaml.j2 b/contrail-service-checks/tests/bundles/overlays/local-charm-overlay.yaml.j2 new file mode 100644 index 0000000..a3020a4 --- /dev/null +++ b/contrail-service-checks/tests/bundles/overlays/local-charm-overlay.yaml.j2 @@ -0,0 +1,3 @@ +applications: + contrail-service-checks: + charm: /tmp/charm-builds/contrail-service-checks \ No newline at end of file diff --git a/contrail-service-checks/tests/tests.yaml b/contrail-service-checks/tests/tests.yaml new file mode 100644 index 0000000..b5f5658 --- /dev/null +++ b/contrail-service-checks/tests/tests.yaml @@ -0,0 +1,11 @@ +charm_name: contrail-service-checks +gate_bundles: + - bionic-queens +smoke_bundles: + - bionic-queens +dev_bundles: + - bionic-queens +configure: + - zaza.charm_tests.noop.setup.basic_setup +tests: + - zaza.charm_tests.noop.tests.NoopTest diff --git a/contrail-service-checks/tox.ini b/contrail-service-checks/tox.ini new file mode 100644 index 0000000..d21beed --- /dev/null +++ b/contrail-service-checks/tox.ini @@ -0,0 +1,46 @@ +[tox] +skipsdist = True +envlist = lint,unit +skip_missing_interpreters = True + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 +basepython = python3 +deps = -r{toxinidir}/test-requirements.txt +whitelist_externals = juju +passenv = + HOME TERM CS_* FIP_RANGE AMULET_* MOJO_* NET_ID GATEWAY NAME_SERVER + CIDR_EXT FIP_RANGE VIP_RANGE +install_command = + pip install {opts} {packages} + +[testenv:build] +commands = + charm-build --log-level DEBUG -o {toxinidir}/build . {posargs} + +[testenv:func] +commands = functest-run-suite --keep-model + +[testenv:func-smoke] +commands = functest-run-suite --keep-model --smoke + +[testenv:unit] +commands = pytest -vv {toxinidir}/unit_tests \ + --cov=lib \ + --cov=reactive \ + --cov=files/plugins \ + --cov-report=term \ + --cov-report=annotate:reports/annotated \ + --cov-report=html:reports/html +setenv = PYTHONPATH={toxinidir}/lib:{toxinidir}/files/plugins:{toxinidir}/reactive + +[testenv:lint] +commands = flake8 files lib reactive unit_tests tests +deps = flake8 + +[flake8] +exclude = + .git, + __pycache__, + .tox, diff --git a/contrail-service-checks/unit_tests/test_check_contrail_alarms.py b/contrail-service-checks/unit_tests/test_check_contrail_alarms.py new file mode 100644 index 0000000..2e677d3 --- /dev/null +++ b/contrail-service-checks/unit_tests/test_check_contrail_alarms.py @@ -0,0 +1,86 @@ +import pytest + +import check_contrail_alarms as cca + + +@pytest.fixture +def keystone_client(mocker): + mocker.patch('check_contrail_alarms.client') + + +def test_check_contrail_with_no_alarms(requests_mock): + alarms = {} + url = 'http://vip:8081/analytics/alarms' + headers = { + 'X-Auth-Token': 'TOKEN' + } + requests_mock.get(url=url, headers=headers, json=alarms) + result = cca.check_contrail_alarms('vip', 'TOKEN') + assert result == 0 + + +def test_check_contrail_with_alarms(requests_mock): + alarms = { + 'config-node': [ + { + 'name': 'node08', + 'value': { + 'UVEAlarms': { + 'alarms': [ + { + 'severity': 0, + 'timestamp': 1558601319354033, + 'ack': False, + 'type': 'default-global:system-connectivity', + 'description': 'Process(es) non-functional.' + } + ], + '__T': 1558601342458515 + } + } + } + ], + 'vrouter': [ + { + 'name': 'node06', + 'value': { + 'UVEAlarms': { + 'alarms': [ + { + 'severity': 0, + 'timestamp': 1558614709553302, + 'ack': False, + 'type': 'default-global-config:something', + 'description': 'Node Failure.' + } + ], + '__T': 1558614709553864 + } + } + } + ] + } + url = 'http://vip:8081/analytics/alarms' + headers = { + 'X-Auth-Token': 'TOKEN' + } + requests_mock.get(url=url, headers=headers, json=alarms) + result = cca.check_contrail_alarms('vip', 'TOKEN') + assert result == 2 + + +def test_get_auth_token(keystone_client): + url = 'url' + user = 'user' + password = 'password' + project = 'project' + domain = 'domain' + cca.get_auth_token(url, user, password, project, domain) + cca.client.Client.assert_called_once_with( + auth_url=url, + username=user, + password=password, + project_name=project, + user_domain_name=domain, + project_domain_name=domain + ) diff --git a/contrail-service-checks/unit_tests/test_contrail_service_checks.py b/contrail-service-checks/unit_tests/test_contrail_service_checks.py new file mode 100644 index 0000000..d8f73d9 --- /dev/null +++ b/contrail-service-checks/unit_tests/test_contrail_service_checks.py @@ -0,0 +1,188 @@ +from unittest.mock import Mock +import pytest + +import contrail_service_checks as csc + + +@pytest.fixture +def get_helper(mocker): + + config = { + 'os-credentials': '', + 'nagios_context': 'juju', + 'nagios_servicegroups': '', + 'trusted_ssl_ca': '', + 'contrail_analytics_vip': 'VIP', + } + + helper = Mock() + helper.store_keystone_credentials = Mock() + helper.charm_config = config + creds = { + 'username': 'username', + 'password': 'password', + 'region': 'region', + 'domain': 'domain', + 'auth_url': 'http://keystone:5000/v3', + 'auth_version': '3', + 'project_name': 'project' + } + helper.get_os_credentials = Mock(return_value=creds) + helper.store_keystone_credentials = Mock() + + mocker.patch('contrail_service_checks.helper', helper) + return helper + + +@pytest.fixture +def service(monkeypatch): + monkeypatch.setattr( + csc.host, + 'service_restart', + lambda x: x + ) + + +@pytest.fixture +def clear_flag(mocker): + mocker.patch('contrail_service_checks.clear_flag') + + +@pytest.fixture +def set_flag(mocker): + mocker.patch('contrail_service_checks.set_flag') + + +@pytest.fixture +def keystone_mock(): + keystone = Mock() + keystone.request_credentials = Mock() + keystone.credentials_username = Mock(return_value='username') + keystone.credentials_password = Mock(return_value='password') + keystone.credentials_project = Mock(return_value='project') + keystone.region = Mock(return_value='region') + keystone.domain = Mock(return_value='domain') + keystone.auth_protocol = Mock(return_value='http') + keystone.auth_port = Mock(return_value='5000') + keystone.auth_host = Mock(return_value='keystone') + return keystone + + +@pytest.fixture +def status_set(mocker): + mocker.patch('contrail_service_checks.hookenv.status_set') + + +@pytest.fixture +def upgrade_charm(monkeypatch): + monkeypatch.setattr( + csc.hookenv, + 'hook_name', + lambda: 'upgrade-charm' + ) + + +@pytest.fixture +def do_reconfigure_nrpe(mocker): + mocker.patch('contrail_service_checks.do_reconfigure_nrpe') + + +def test_config_changed(clear_flag): + csc.config_changed() + csc.clear_flag.assert_called_once_with( + 'contrail-service-checks.configured' + ) + + +def test_install_contrail_service_checks(clear_flag, set_flag): + csc.install_contrail_service_checks() + csc.set_flag.assert_called_once_with( + 'contrail-service-checks.installed' + ) + csc.clear_flag.assert_called_once_with( + 'contrail-service-checks.configured' + ) + + +def test_configure_ident_username(clear_flag, keystone_mock): + csc.configure_ident_username(keystone_mock) + keystone_mock.request_credentials.assert_called_once_with( + 'nagios-contrail' + ) + csc.clear_flag.assert_called_once_with( + 'contrail-service-checks.stored-creds' + ) + + +def test_save_creds(clear_flag, set_flag, keystone_mock, get_helper): + csc.save_creds(keystone_mock) + expected_creds = { + 'username': 'username', + 'password': 'password', + 'region': 'region', + 'domain': 'domain', + 'auth_url': 'http://keystone:5000/v3', + 'auth_version': '3', + 'project_name': 'project' + } + get_helper.store_keystone_credentials.assert_called_once_with( + expected_creds) + + +def test_allow_keystone_store_overwrite(clear_flag): + csc.allow_keystone_store_overwrite() + csc.clear_flag.assert_called_once_with( + 'contrail-service-checks.stored-creds' + ) + + +def test_get_credentials(get_helper): + result = csc.get_credentials() + expected_creds = { + 'username': 'username', + 'password': 'password', + 'region': 'region', + 'domain': 'domain', + 'auth_url': 'http://keystone:5000/v3', + 'auth_version': '3', + 'project_name': 'project', + 'contrail_analytics_vip': 'VIP' + } + assert result == expected_creds + + +def test_render_config(get_helper, set_flag, clear_flag): + csc.render_config() + csc.set_flag.assert_called_once_with( + 'contrail-service-checks.configured' + ) + csc.clear_flag.assert_called_once_with( + 'contrail-service-checks.started' + ) + + +def test_do_restart(set_flag, service): + csc.do_restart() + csc.set_flag.assert_called_once_with( + 'contrail-service-checks.started' + ) + + +def test_do_reconfigure_nrpe(clear_flag): + csc.do_reconfigure_nrpe() + csc.clear_flag.assert_called_once_with( + 'contrail-service-checks.configured' + ) + + +def test_missing_nrpe(status_set): + csc.missing_nrpe() + csc.hookenv.status_set.assert_called_once_with( + 'blocked', + 'Missing relations: nrpe' + ) + + +def test_upgrade_charm(upgrade_charm, do_reconfigure_nrpe): + csc.parse_hooks() + csc.do_reconfigure_nrpe.assert_called_once() diff --git a/contrail-service-checks/unit_tests/test_lib_contrail_service_checks.py b/contrail-service-checks/unit_tests/test_lib_contrail_service_checks.py new file mode 100644 index 0000000..4cb1dd3 --- /dev/null +++ b/contrail-service-checks/unit_tests/test_lib_contrail_service_checks.py @@ -0,0 +1,155 @@ +import charmhelpers +import getpass +import os +import pytest + +import lib_contrail_service_checks as lcsc + + +@pytest.fixture +def generate_helper(monkeypatch): + def config(): + return { + 'os-credentials': '', + 'nagios_context': 'juju', + 'nagios_servicegroups': '', + 'trusted_ssl_ca': '', + 'contrail_analytics_vip': 'VIP', + } + + def render(source, target, context, owner, group): + owner = getpass.getuser() + return charmhelpers.core.templating.render( + source, + target, + context, + owner, + owner + ) + + def relation_ids(args): + return {} + + def makedirs(args): + return + + def rsync(from_path, to_path, options): + return + monkeypatch.setattr( + lcsc.hookenv, + 'config', + config + ) + monkeypatch.setattr( + charmhelpers.contrib.charmsupport.nrpe, + 'config', + config + ) + monkeypatch.setattr( + charmhelpers.contrib.charmsupport.nrpe, + 'local_unit', + lambda: 'unit/0' + ) + monkeypatch.setattr( + charmhelpers.contrib.charmsupport.nrpe, + 'get_nagios_hostname', + lambda: 'nagios' + ) + monkeypatch.setattr( + charmhelpers.contrib.charmsupport.nrpe, + 'relation_ids', + relation_ids + ) + monkeypatch.setattr( + charmhelpers.core.hookenv, + 'charm_dir', + lambda: '.' + ) + monkeypatch.setattr( + lcsc, + 'render', + render + ) + monkeypatch.setattr( + os, + 'makedirs', + makedirs + ) + monkeypatch.setattr( + lcsc.host, + 'rsync', + rsync + ) + return lcsc.CSCHelper() + + +def test_helper_properties(generate_helper): + assert generate_helper.oscreds == '/var/lib/nagios/keystone.yaml' + assert generate_helper.plugins_dir == '/usr/local/lib/nagios/plugins/' + + +def test_can_store_keystone_credentials(generate_helper): + creds = {} + generate_helper.store_keystone_credentials(creds) + + +def test_get_os_credentials_raises_exception_with_defaults(generate_helper): + with pytest.raises(lcsc.CSCCredentialsError): + generate_helper.get_os_credentials() + + +def test_get_os_credentials_with_missing_attributes(generate_helper): + # Domain is missing from the credentials + os_credentials = ('username=foo, password=bar, credentials_project=baz,' + ' region_name=Region1, auth_url=http://keystone:5000/v3') + generate_helper.charm_config['os-credentials'] = os_credentials + with pytest.raises(lcsc.CSCCredentialsError): + generate_helper.get_os_credentials() + + +def test_get_os_credentials_with_v3(generate_helper): + os_credentials = ('username=foo, password=bar, project_name=baz,' + ' region_name=Region1, auth_url=http://keystone:5000/v3,' + ' domain=domain') + generate_helper.charm_config['os-credentials'] = os_credentials + expected = { + 'auth_url': 'http://keystone:5000/v3', + 'project_name': 'baz', + 'username': 'foo', + 'password': 'bar', + 'domain': 'domain', + 'region_name': 'Region1' + } + assert generate_helper.get_os_credentials() == expected + + +def test_get_keystone_credentials(generate_helper): + assert generate_helper.get_keystone_credentials() == {} + + +def test_render_checks(generate_helper, tmpdir, monkeypatch): + creds = { + 'auth_url': 'http://keystone:5000/v3', + 'project_name': 'baz', + 'username': 'foo', + 'password': 'bar', + 'domain': 'domain', + 'region_name': 'Region1', + 'contrail_analytics_vip': 'vip' + } + + oscreds = tmpdir.mkdir('oscreds').join('keystone.yaml') + + monkeypatch.setattr( + lcsc.CSCHelper, + 'oscreds', + property(lambda x: oscreds) + ) + generate_helper.render_checks(creds) + + expected = ('user: foo\nproject: baz\npassword: bar\n' + 'auth_url: http://keystone:5000/v3\n' + 'domain: domain\ncontrail_analytics_vip: vip') + + with open(oscreds) as f: + assert f.read() == expected