diff --git a/vitrage_tempest_tests/tests/common/general_utils.py b/vitrage_tempest_tests/tests/common/general_utils.py new file mode 100644 index 0000000..3cf3527 --- /dev/null +++ b/vitrage_tempest_tests/tests/common/general_utils.py @@ -0,0 +1,30 @@ +# Copyright 2017 - Nokia +# +# 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. +import six + + +def get_first_match(list_of_dicts, subset_dict): + for d in list_of_dicts: + if is_subset(subset_dict, d): + return d + + +def get_all_matchs(list_of_dicts, subset_dict): + # TODO(idan_hefetz) this method can replace the notorious + # TODO(idan_hefetz) '_filter_list_by_pairs_parameters' + return [d for d in list_of_dicts if is_subset(subset_dict, d)] + + +def is_subset(subset, full): + return six.viewitems(subset) <= six.viewitems(full) diff --git a/vitrage_tempest_tests/tests/common/tempest_clients.py b/vitrage_tempest_tests/tests/common/tempest_clients.py index af0c667..2440fa1 100644 --- a/vitrage_tempest_tests/tests/common/tempest_clients.py +++ b/vitrage_tempest_tests/tests/common/tempest_clients.py @@ -33,6 +33,10 @@ class TempestClients(object): @classmethod def vitrage(cls): + """vitrage client + + :rtype: vitrageclient.v1.client.Client + """ if not cls._vitrage: cls._vitrage = vc.Client( '1', session=keystone_client.get_session(cls._conf)) @@ -40,48 +44,80 @@ class TempestClients(object): @classmethod def ceilometer(cls): + """ceilometer client + + :rtype: ceilometerclient.v2.client.Client + """ if not cls._ceilometer: cls._ceilometer = os_clients.ceilometer_client(cls._conf) return cls._ceilometer @classmethod def nova(cls): + """nova client + + :rtype: novaclient.v2.client.Client + """ if not cls._nova: cls._nova = os_clients.nova_client(cls._conf) return cls._nova @classmethod def cinder(cls): + """cinder client + + :rtype: cinderclient.v2.client.Client + """ if not cls._cinder: cls._cinder = os_clients.cinder_client(cls._conf) return cls._cinder @classmethod def glance(cls): + """glance client + + :rtype: glanceclient.v2.client.Client + """ if not cls._glance: cls._glance = os_clients.glance_client(cls._conf) return cls._glance @classmethod def neutron(cls): + """neutron client + + :rtype: neutronclient.v2_0.client.Client + """ if not cls._neutron: cls._neutron = os_clients.neutron_client(cls._conf) return cls._neutron @classmethod def heat(cls): + """heat client + + :rtype: heatclient.v1.client.Client + """ if not cls._heat: cls._heat = os_clients.heat_client(cls._conf) return cls._heat @classmethod def mistral(cls): + """mistral client + + :rtype: mistralclient.v2.client.Client + """ if not cls._mistral: cls._mistral = os_clients.mistral_client(cls._conf) return cls._mistral @classmethod def aodh(cls): + """aodh client + + :rtype: aodhclient.v2.client.Client + """ if not cls._aodh: cls._aodh = os_clients.aodh_client(cls._conf) return cls._aodh diff --git a/vitrage_tempest_tests/tests/common/vitrage_utils.py b/vitrage_tempest_tests/tests/common/vitrage_utils.py index 94406f8..15030a0 100644 --- a/vitrage_tempest_tests/tests/common/vitrage_utils.py +++ b/vitrage_tempest_tests/tests/common/vitrage_utils.py @@ -11,13 +11,31 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime + from vitrage.common.constants import VertexProperties as VProps from vitrage.datasources import NOVA_HOST_DATASOURCE +from vitrage_tempest_tests.tests.api.event.base import DOWN +from vitrage_tempest_tests.tests.api.event.base import UP +from vitrage_tempest_tests.tests.common.general_utils import get_first_match from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients +def generate_fake_host_alarm(hostname, event_type, enabled=True): + details = { + 'hostname': hostname, + 'source': 'fake_tempest_monitor', + 'cause': 'another alarm', + 'severity': 'critical', + 'status': DOWN if enabled else UP, + 'monitor_id': 'fake tempest monitor id', + 'monitor_event_id': '111', + } + event_time = datetime.now() + event_time_iso = event_time.isoformat() + TempestClients.vitrage().event.post(event_time_iso, event_type, details) + + def get_first_host(): nodes = TempestClients.vitrage().topology.get(all_tenants=True)['nodes'] - return next( - (n for n in nodes if n[VProps.VITRAGE_TYPE] == NOVA_HOST_DATASOURCE), - None) + return get_first_match(nodes, {VProps.VITRAGE_TYPE: NOVA_HOST_DATASOURCE}) diff --git a/vitrage_tempest_tests/tests/e2e/__init__.py b/vitrage_tempest_tests/tests/e2e/__init__.py new file mode 100644 index 0000000..bf9f61d --- /dev/null +++ b/vitrage_tempest_tests/tests/e2e/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2017 - Nokia +# +# 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. + +__author__ = 'stack' diff --git a/vitrage_tempest_tests/tests/e2e/test_basic_actions.py b/vitrage_tempest_tests/tests/e2e/test_basic_actions.py new file mode 100644 index 0000000..aa610e9 --- /dev/null +++ b/vitrage_tempest_tests/tests/e2e/test_basic_actions.py @@ -0,0 +1,189 @@ +# Copyright 2017 - Nokia +# +# 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. +import time + +from oslo_log import log as logging + +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources.doctor import DOCTOR_DATASOURCE +from vitrage.evaluator.actions.evaluator_event_transformer import \ + VITRAGE_DATASOURCE +from vitrage_tempest_tests.tests.base import BaseVitrageTempest +from vitrage_tempest_tests.tests.common import general_utils as g_utils +from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients +from vitrage_tempest_tests.tests.common import vitrage_utils +from vitrage_tempest_tests.tests import utils + +LOG = logging.getLogger(__name__) + +TRIGGER_ALARM_1 = 'e2e.test_basic_actions.trigger.alarm1' +TRIGGER_ALARM_2 = 'e2e.test_basic_actions.trigger.alarm2' +DEDUCED = 'e2e.test_basic_actions.deduced.alarm' + +TRIGGER_ALARM_2_PROPS = { + VProps.NAME: TRIGGER_ALARM_2, + VProps.VITRAGE_CATEGORY: EntityCategory.ALARM, + VProps.VITRAGE_TYPE: DOCTOR_DATASOURCE, +} + +DEDUCED_PROPS = { + VProps.NAME: DEDUCED, + VProps.VITRAGE_CATEGORY: EntityCategory.ALARM, + VProps.VITRAGE_TYPE: VITRAGE_DATASOURCE, +} + + +class TestBasicActions(BaseVitrageTempest): + @classmethod + def setUpClass(cls): + super(TestBasicActions, cls).setUpClass() + host = vitrage_utils.get_first_host() + if not host: + raise Exception("No host found") + if not host.get(VProps.VITRAGE_AGGREGATED_STATE) == 'AVAILABLE': + raise Exception("Host is not running %s", str(host)) + cls.orig_host = host + + def _trigger_do_action(self, trigger_name): + vitrage_utils.generate_fake_host_alarm( + self.orig_host.get('name'), + enabled=True, + event_type=trigger_name + ) + time.sleep(2) + + def _trigger_undo_action(self, trigger_name): + vitrage_utils.generate_fake_host_alarm( + self.orig_host.get('name'), + enabled=False, + event_type=trigger_name + ) + time.sleep(2) + + @utils.tempest_logger + def test_action_set_state(self): + try: + + # Do + self._trigger_do_action(TRIGGER_ALARM_1) + curr_host = vitrage_utils.get_first_host() + self.assertEqual( + 'ERROR', + curr_host.get(VProps.VITRAGE_AGGREGATED_STATE), + 'state should change after set_state action') + + # Undo + self._trigger_undo_action(TRIGGER_ALARM_1) + curr_host = vitrage_utils.get_first_host() + self.assertEqual( + self.orig_host.get(VProps.VITRAGE_AGGREGATED_STATE), + curr_host.get(VProps.VITRAGE_AGGREGATED_STATE), + 'state should change after set_state action') + except Exception as e: + self._handle_exception(e) + raise + finally: + self._trigger_undo_action(TRIGGER_ALARM_1) + + @utils.tempest_logger + def test_action_mark_down_host(self): + try: + host_name = self.orig_host.get(VProps.NAME) + + # Do + self._trigger_do_action(TRIGGER_ALARM_1) + nova_service = TempestClients.nova().services.list( + host=host_name, binary='nova-compute')[0] + self.assertEqual("down", str(nova_service.state)) + + # Undo + self._trigger_undo_action(TRIGGER_ALARM_1) + nova_service = TempestClients.nova().services.list( + host=host_name, binary='nova-compute')[0] + self.assertEqual("up", str(nova_service.state)) + except Exception as e: + self._handle_exception(e) + raise + finally: + self._trigger_undo_action(TRIGGER_ALARM_1) + + @utils.tempest_logger + def test_action_deduce_alarm(self): + try: + host_id = self.orig_host.get(VProps.VITRAGE_ID) + + # Do + self._trigger_do_action(TRIGGER_ALARM_2) + self._check_deduced(1, DEDUCED_PROPS, host_id) + + # Undo + self._trigger_undo_action(TRIGGER_ALARM_2) + self._check_deduced(0, DEDUCED_PROPS, host_id) + except Exception as e: + self._handle_exception(e) + raise + finally: + self._trigger_undo_action(TRIGGER_ALARM_2) + + def _check_deduced(self, deduced_count, deduced_props, resource_id): + alarms = TempestClients.vitrage().alarm.list( + vitrage_id=resource_id, + all_tenants=True) + deduces = g_utils.get_all_matchs(alarms, deduced_props) + self.assertEqual( + deduced_count, + len(deduces), + 'Expected %s deduces\n - \n%s\n - \n%s' % + (str(deduced_count), str(alarms), str(deduces))) + + @utils.tempest_logger + def test_action_add_causal_relationship(self): + try: + # Do + self._trigger_do_action(TRIGGER_ALARM_2) + alarms = TempestClients.vitrage().alarm.list( + vitrage_id=self.orig_host.get(VProps.VITRAGE_ID), + all_tenants=True) + + deduced = g_utils.get_all_matchs(alarms, DEDUCED_PROPS)[0] + trigger = g_utils.get_all_matchs(alarms, TRIGGER_ALARM_2_PROPS)[0] + + # Get Rca for the deduced + rca = TempestClients.vitrage().rca.get( + deduced[VProps.VITRAGE_ID], all_tenants=True) + self._check_rca(rca, [deduced, trigger], DEDUCED_PROPS) + + # Get Rca for the trigger + rca = TempestClients.vitrage().rca.get( + trigger[VProps.VITRAGE_ID], all_tenants=True) + self._check_rca(rca, [deduced, trigger], TRIGGER_ALARM_2_PROPS) + except Exception as e: + self._handle_exception(e) + raise + finally: + self._trigger_undo_action(TRIGGER_ALARM_2) + + def _check_rca(self, rca, expected_alarms, inspected): + self.assertEqual(len(expected_alarms), len(rca['nodes'])) + for expected_alarm in expected_alarms: + self.assertIsNotNone( + g_utils.get_first_match(rca['nodes'], expected_alarm), + 'expected_alarm is not in the rca %s' % str(expected_alarm)) + rca_inspected = rca['nodes'][rca['inspected_index']] + self.assertEqual( + True, + g_utils.is_subset(inspected, rca_inspected), + 'Invalid inspected item \n%s\n%s' % + (str(rca_inspected), str(inspected))) diff --git a/vitrage_tempest_tests/tests/resources/templates/api/e2e_test_basic_actions.yaml b/vitrage_tempest_tests/tests/resources/templates/api/e2e_test_basic_actions.yaml new file mode 100644 index 0000000..04691e3 --- /dev/null +++ b/vitrage_tempest_tests/tests/resources/templates/api/e2e_test_basic_actions.yaml @@ -0,0 +1,70 @@ +metadata: + name: e2e_test_basic_actions + description: this template includes vitrage basic actions +definitions: + entities: + - entity: + category: ALARM + name: e2e.test_basic_actions.trigger.alarm1 + template_id: trigger_alarm_1 + - entity: + category: ALARM + name: e2e.test_basic_actions.trigger.alarm2 + template_id: trigger_alarm_2 + - entity: + category: ALARM + type: vitrage + name: e2e.test_basic_actions.deduced.alarm + template_id: deduced_alarm + - entity: + category: RESOURCE + type: nova.host + template_id: host + relationships: + - relationship: + source: trigger_alarm_1 + relationship_type: on + target: host + template_id : trigger_alarm_1_on_host + - relationship: + source: trigger_alarm_2 + relationship_type: on + target: host + template_id : trigger_alarm_2_on_host + - relationship: + source: deduced_alarm + relationship_type: on + target: host + template_id : deduced_alarm_on_host +scenarios: + - scenario: + condition: trigger_alarm_1_on_host + actions: + - action: + action_type: set_state + action_target: + target: host + properties: + state: ERROR + - action: + action_type: mark_down + action_target: + target: host + - scenario: + condition: trigger_alarm_2_on_host + actions: + - action: + action_type: raise_alarm + action_target: + target: host + properties: + alarm_name: e2e.test_basic_actions.deduced.alarm + severity: WARNING + - scenario: + condition: trigger_alarm_2_on_host and deduced_alarm_on_host + actions: + - action: + action_type: add_causal_relationship + action_target: + source: trigger_alarm_2 + target: deduced_alarm diff --git a/vitrage_tempest_tests/tests/utils.py b/vitrage_tempest_tests/tests/utils.py index 8e1f361..f42947e 100644 --- a/vitrage_tempest_tests/tests/utils.py +++ b/vitrage_tempest_tests/tests/utils.py @@ -17,12 +17,10 @@ from functools import wraps import socket import time -from oslo_config import cfg from oslo_config.cfg import NoSuchOptError from oslo_log import log as logging import os -import oslo_messaging import re import subprocess @@ -115,13 +113,6 @@ def change_terminal_dir(path): LOG.debug("The path is : " + path) -def get_client(): - transport = oslo_messaging.get_transport(cfg.CONF) - cfg.CONF.set_override('rpc_backend', 'rabbit') - target = oslo_messaging.Target(topic='rpcapiv1') - return oslo_messaging.RPCClient(transport, target) - - def get_regex_result(pattern, text): p = re.compile(pattern) m = p.search(text)