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 @@
+
+
+
+
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