diff --git a/setup.py b/setup.py index 0980b96c9..20679d4df 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ 'PyYAML', 'tenacity', 'oslo.config', + 'aodhclient', 'python-glanceclient', 'python-keystoneclient', 'python-novaclient', @@ -102,4 +103,4 @@ def run_tests(self): 'testing': tests_require, }, tests_require=tests_require, -) \ No newline at end of file +) diff --git a/zaza/openstack/charm_tests/aodh/__init__.py b/zaza/openstack/charm_tests/aodh/__init__.py new file mode 100644 index 000000000..2781b7bdb --- /dev/null +++ b/zaza/openstack/charm_tests/aodh/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Canonical Ltd. +# +# 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. + +"""Collection of code for setting up and testing aodh.""" diff --git a/zaza/openstack/charm_tests/aodh/tests.py b/zaza/openstack/charm_tests/aodh/tests.py new file mode 100644 index 000000000..0ba24af84 --- /dev/null +++ b/zaza/openstack/charm_tests/aodh/tests.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +# Copyright 2019 Canonical Ltd. +# +# 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. + +"""Encapsulate Aodh testing.""" + +import logging + +import zaza.model +import zaza.openstack.charm_tests.test_utils as test_utils +import zaza.openstack.utilities.openstack as openstack_utils +import zaza.openstack.configure.telemetry as telemetry_utils + + +class AodhTest(test_utils.OpenStackBaseTest): + """Encapsulate Aodh tests.""" + + RESOURCE_PREFIX = 'zaza-aodhtests' + + @classmethod + def setUpClass(cls): + """Run class setup for running tests.""" + super(AodhTest, cls).setUpClass() + cls.xenial_ocata = openstack_utils.get_os_release('xenial_ocata') + cls.xenial_newton = openstack_utils.get_os_release('xenial_newton') + cls.bionic_stein = openstack_utils.get_os_release('bionic_stein') + cls.release = openstack_utils.get_os_release() + cls.keystone_session = openstack_utils.get_overcloud_keystone_session() + cls.model_name = zaza.model.get_juju_model() + cls.aodh_client = openstack_utils.get_aodh_session_client( + cls.keystone_session) + + @classmethod + def tearDown(cls): + """Remove test resources.""" + logging.info('Running teardown') + cache_wait = False + for alarm in cls.aodh_client.alarm.list(): + if alarm['name'].startswith(cls.RESOURCE_PREFIX): + cache_wait = True + logging.info('Removing Alarm {}'.format(alarm['name'])) + telemetry_utils.delete_alarm( + cls.aodh_client, + alarm['name'], + cache_wait=False) + if cache_wait: + logging.info('Waiting for alarm cache to clear') + telemetry_utils.alarm_cache_wait() + + @property + def services(self): + """Return a list of the service that should be running.""" + if self.release >= self.xenial_ocata: + services = [ + 'apache2', + 'aodh-evaluator: AlarmEvaluationService worker(0)', + 'aodh-notifier: AlarmNotifierService worker(0)', + ('aodh-listener: EventAlarmEvaluationService' + ' worker(0)')] + elif self.release >= self.xenial_newton: + services = [ + ('/usr/bin/python /usr/bin/aodh-api --port 8032 -- ' + '--config-file=/etc/aodh/aodh.conf ' + '--log-file=/var/log/aodh/aodh-api.log'), + 'aodh-evaluator - AlarmEvaluationService(0)', + 'aodh-notifier - AlarmNotifierService(0)', + 'aodh-listener - EventAlarmEvaluationService(0)'] + else: + services = [ + 'aodh-api', + 'aodh-evaluator', + 'aodh-notifier', + 'aodh-listener'] + return services + + def test_100_test_api(self): + """Check api by creating an alarm.""" + alarm_name = '{}_test_api_alarm'.format(self.RESOURCE_PREFIX) + logging.info('Creating alarm {}'.format(alarm_name)) + alarm = telemetry_utils.create_server_power_off_alarm( + self.aodh_client, + alarm_name, + 'some-uuid') + alarm_state = telemetry_utils.get_alarm_state( + self.aodh_client, + alarm['alarm_id']) + logging.info('alarm_state: {}'.format(alarm_state)) + # Until data is collected alarm come up in an 'insufficient data' + # state. + self.assertEqual(alarm_state, 'insufficient data') + + def test_900_restart_on_config_change(self): + """Checking restart happens on config change. + + Change disk format and assert then change propagates to the correct + file and that services are restarted as a result + """ + # Expected default and alternate values + set_default = {'debug': 'False'} + set_alternate = {'debug': 'True'} + + # Config file affected by juju set config change + conf_file = '/etc/aodh/aodh.conf' + + # Make config change, check for service restarts + self.restart_on_changed( + conf_file, + set_default, + set_alternate, + {'DEFAULT': {'debug': ['False']}}, + {'DEFAULT': {'debug': ['True']}}, + self.services) + + def test_901_pause_resume(self): + """Run pause and resume tests. + + Pause service and check services are stopped then resume and check + they are started + """ + with self.pause_resume( + self.services, + pgrep_full=False): + logging.info("Testing pause resume") diff --git a/zaza/openstack/configure/telemetry.py b/zaza/openstack/configure/telemetry.py new file mode 100644 index 000000000..86102acb4 --- /dev/null +++ b/zaza/openstack/configure/telemetry.py @@ -0,0 +1,121 @@ +# Copyright 2019 Canonical Ltd. +# +# 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. + +"""Configure and manage masakari. + +Functions for managing masakari resources and simulating compute node loss +and recovery. +""" + +import time + +import zaza.model + + +def ceilometer_upgrade(application_name=None, model_name=None): + """Run ceilometer upgrade action. + + :param application_name: Name of application to run action against. + :type application_name: str + :param model_name: Name of model application_name resides in. + :type model_name: str + """ + zaza.model.run_action_on_leader( + application_name, + 'ceilometer-upgrade', + model_name=model_name, + action_params={}) + + +def get_alarm(aodh_client, alarm_name): + """Return the alarm with the given name. + + :param aodh_client: Authenticated aodh v2 client + :type aodh_client: aodhclient.v2.client.Client + :param alarm_name: Name of alarm to search for + :type alarm_name: str + :returns: Returns a dict of alarm data. + :rtype: {} or None + """ + for alarm in aodh_client.alarm.list(): + if alarm['name'] == alarm_name: + return alarm + return None + + +def alarm_cache_wait(): + """Wait for alarm cache to clear.""" + # AODH has an alarm cache (see event_alarm_cache_ttl in aodh.conf). This + # means deleted alarms can persist and fire. The default is 60s and is + # currently not configrable via the charm so 61s is a safe assumption. + time.sleep(61) + + +def delete_alarm(aodh_client, alarm_name, cache_wait=False): + """Delete alarm with given name. + + :param aodh_client: Authenticated aodh v2 client + :type aodh_client: aodhclient.v2.client.Client + :param alarm_name: Name of alarm to delete + :type alarm_name: str + :param cache_wait: Whether to wait for cache to clear after deletion. + :type cache_wait: bool + """ + alarm = get_alarm(aodh_client, alarm_name) + if alarm: + aodh_client.alarm.delete(alarm['alarm_id']) + if cache_wait: + alarm_cache_wait() + + +def get_alarm_state(aodh_client, alarm_id): + """Return the state of the alarm with the given name. + + :param aodh_client: Authenticated aodh v2 client + :type aodh_client: aodhclient.v2.client.Client + :param alarm_id: ID of provided alarm + :param alarm_id: str + :returns: State of given alarm + :rtype: str + """ + alarm = aodh_client.alarm.get(alarm_id) + return alarm['state'] + + +def create_server_power_off_alarm(aodh_client, alarm_name, server_uuid): + """Create an alarm which triggers when an instance powers off. + + :param aodh_client: Authenticated aodh v2 client + :type aodh_client: aodhclient.v2.client.Client + :param alarm_name: Name of alarm to delete + :type alarm_name: str + :param server_uuid: UUID of server to monitor + :type server_uuid: str + :returns: Dict of alarm data + :rtype: {} + """ + alarm_def = { + 'type': 'event', + 'name': alarm_name, + 'description': 'Instance powered OFF', + 'alarm_actions': ['log://'], + 'ok_actions': ['log://'], + 'insufficient_data_actions': ['log://'], + 'event_rule': { + 'event_type': 'compute.instance.power_off.*', + 'query': [{'field': 'traits.instance_id', + 'op': 'eq', + 'type': 'string', + 'value': server_uuid}]}} + return aodh_client.alarm.create(alarm_def) diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index 26b6aaf19..f0b7ffcb3 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -25,6 +25,7 @@ from openstack import connection +from aodhclient.v2 import client as aodh_client from cinderclient import client as cinderclient from glanceclient import Client as GlanceClient @@ -289,6 +290,17 @@ def get_masakari_session_client(session, interface='internal', return conn.instance_ha +def get_aodh_session_client(session): + """Return aodh client authenticated by keystone session. + + :param session: Keystone session object + :type session: keystoneauth1.session.Session object + :returns: Authenticated aodh client + :rtype: openstack.instance_ha.v1._proxy.Proxy + """ + return aodh_client.Client(session=session) + + def get_keystone_scope(): """Return Keystone scope based on OpenStack release of the overcloud.