diff --git a/vitrage_tempest_tests/tests/common/constants.py b/vitrage_tempest_tests/tests/common/constants.py new file mode 100644 index 0000000..229b827 --- /dev/null +++ b/vitrage_tempest_tests/tests/common/constants.py @@ -0,0 +1,182 @@ +# Copyright 2015 - Alcatel-Lucent +# Copyright 2016 - 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. + + +class VertexProperties(object): + VITRAGE_CATEGORY = 'vitrage_category' + VITRAGE_TYPE = 'vitrage_type' + VITRAGE_ID = 'vitrage_id' + VITRAGE_STATE = 'vitrage_state' + VITRAGE_IS_DELETED = 'vitrage_is_deleted' + VITRAGE_IS_PLACEHOLDER = 'vitrage_is_placeholder' + VITRAGE_SAMPLE_TIMESTAMP = 'vitrage_sample_timestamp' + VITRAGE_AGGREGATED_STATE = 'vitrage_aggregated_state' + VITRAGE_OPERATIONAL_STATE = 'vitrage_operational_state' + VITRAGE_AGGREGATED_SEVERITY = 'vitrage_aggregated_severity' + VITRAGE_OPERATIONAL_SEVERITY = 'vitrage_operational_severity' + VITRAGE_RESOURCE_ID = 'vitrage_resource_id' + ID = 'id' + STATE = 'state' + PROJECT_ID = 'project_id' + UPDATE_TIMESTAMP = 'update_timestamp' + NAME = 'name' + SEVERITY = 'severity' + IS_MARKED_DOWN = 'is_marked_down' + INFO = 'info' + GRAPH_INDEX = 'graph_index' + RAWTEXT = 'rawtext' + RESOURCE_ID = 'resource_id' + RESOURCE_NAME = 'resource_name' + VITRAGE_RESOURCE_TYPE = 'vitrage_resource_type' + RESOURCE = 'resource' + IS_REAL_VITRAGE_ID = 'is_real_vitrage_id' + + +class EdgeProperties(object): + RELATIONSHIP_TYPE = 'relationship_type' + VITRAGE_IS_DELETED = 'vitrage_is_deleted' + UPDATE_TIMESTAMP = 'update_timestamp' + + +class EdgeLabel(object): + """Define *some* edge labels + + Note that edge labels are not restricted to the values in this class, and + other datasources can defined their own edge labels. + """ + ON = 'on' + CONTAINS = 'contains' + CAUSES = 'causes' + ATTACHED = 'attached' + ATTACHED_PUBLIC = 'attached_public' + ATTACHED_PRIVATE = 'attached_private' + CONNECT = 'connect' + MANAGED_BY = 'managed_by' + COMPRISED = 'comprised' + + @staticmethod + def labels(): + return [value for label, value in vars(EdgeLabel).items() + if not label.startswith(('_', 'labels'))] + + +class DatasourceAction(object): + SNAPSHOT = 'snapshot' + INIT_SNAPSHOT = 'init_snapshot' + UPDATE = 'update' + + +class UpdateMethod(object): + NONE = 'none' + PULL = 'pull' + PUSH = 'push' + + +class EntityCategory(object): + RESOURCE = 'RESOURCE' + ALARM = 'ALARM' + + @staticmethod + def categories(): + return [value for category, value in vars(EntityCategory).items() + if not category.startswith(('_', 'categories'))] + + +class DatasourceProperties(object): + ENTITY_TYPE = 'vitrage_entity_type' + DATASOURCE_ACTION = 'vitrage_datasource_action' + SAMPLE_DATE = 'vitrage_sample_date' + EVENT_TYPE = 'vitrage_event_type' + + +class GraphAction(object): + CREATE_ENTITY = 'create_entity' + DELETE_ENTITY = 'delete_entity' + UPDATE_ENTITY = 'update_entity' + DELETE_RELATIONSHIP = 'delete_relationship' + UPDATE_RELATIONSHIP = 'update_relationship' + REMOVE_DELETED_ENTITY = 'remove_deleted_entity' + END_MESSAGE = 'end_message' + + +class NotifierEventTypes(object): + ACTIVATE_DEDUCED_ALARM_EVENT = 'vitrage.deduced_alarm.activate' + DEACTIVATE_DEDUCED_ALARM_EVENT = 'vitrage.deduced_alarm.deactivate' + ACTIVATE_ALARM_EVENT = 'vitrage.alarm.activate' + DEACTIVATE_ALARM_EVENT = 'vitrage.alarm.deactivate' + ACTIVATE_MARK_DOWN_EVENT = 'vitrage.mark_down.activate' + DEACTIVATE_MARK_DOWN_EVENT = 'vitrage.mark_down.deactivate' + EXECUTE_EXTERNAL_ACTION = 'vitrage.execute_external_action' + + +class TemplateTopologyFields(object): + """yaml fields for topology definitions""" + METADATA = 'metadata' + DESCRIPTION = 'description' + NAME = 'name' + VERSION = 'version' + + DEFINITIONS = 'definitions' + + ENTITIES = 'entities' + ENTITY = 'entity' + TYPE = 'type' + ID = 'id' + + RELATIONSHIPS = 'relationships' + RELATIONSHIP = 'relationship' + RELATIONSHIP_TYPE = 'relationship_type' + SOURCE = 'source' + TARGET = 'target' + + +class EventProperties(object): + TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' + TYPE = 'type' + TIME = 'time' + DETAILS = 'details' + + +class DatasourceOpts(object): + TRANSFORMER = 'transformer' + DRIVER = 'driver' + UPDATE_METHOD = 'update_method' + CHANGES_INTERVAL = 'changes_interval' + CONFIG_FILE = 'config_file' + + +class TemplateTypes(object): + STANDARD = 'standard' + DEFINITION = 'definition' + EQUIVALENCE = 'equivalence' + + @staticmethod + def types(): + return [value for type, value in vars(TemplateTypes).items() + if not type.startswith(('_', 'types'))] + + +class TemplateStatus(object): + ACTIVE = 'ACTIVE' + ERROR = 'ERROR' + DELETING = 'DELETING' + DELETED = 'DELETED' + LOADING = 'LOADING' + + +class TenantProps(object): + ALL_TENANTS = 'all_tenants' + TENANT = 'tenant' + IS_ADMIN = 'is_admin' diff --git a/vitrage_tempest_tests/tests/e2e/test_e2e_webhook.py b/vitrage_tempest_tests/tests/e2e/test_e2e_webhook.py index 46fcbea..6ab640e 100644 --- a/vitrage_tempest_tests/tests/e2e/test_e2e_webhook.py +++ b/vitrage_tempest_tests/tests/e2e/test_e2e_webhook.py @@ -11,17 +11,24 @@ # 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 ast from oslo_log import log as logging import requests from six.moves import BaseHTTPServer import socket from threading import Thread +import time +from vitrage_tempest_tests.tests.common.constants import VertexProperties as \ + VProps from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients +from vitrage_tempest_tests.tests.common import vitrage_utils as v_utils + from vitrage_tempest_tests.tests.e2e.test_actions_base import TestActionsBase from vitrage_tempest_tests.tests import utils + LOG = logging.getLogger(__name__) TRIGGER_ALARM_1 = 'e2e.test_webhook.alarm1' @@ -35,6 +42,26 @@ NAME_FILTER = '{"name": "e2e.*"}' NAME_FILTER_FOR_DEDUCED = '{"name": "e2e.test_webhook.deduced"}' TYPE_FILTER = '{"vitrage_type": "doctor"}' FILTER_NO_MATCH = '{"name": "NO MATCH"}' +NOTIFICATION = 'notification' +PAYLOAD = 'payload' +MAIN_FILTER = (NOTIFICATION, + PAYLOAD) +DOCTOR_ALARM_FILTER = (VProps.VITRAGE_ID, + VProps.RESOURCE, + VProps.NAME, + VProps.UPDATE_TIMESTAMP, + VProps.VITRAGE_TYPE, + VProps.VITRAGE_CATEGORY, + VProps.STATE, + VProps.VITRAGE_OPERATIONAL_SEVERITY) +RESOURCE_FILTER = (VProps.VITRAGE_ID, + VProps.ID, + VProps.NAME, + VProps.UPDATE_TIMESTAMP, + VProps.VITRAGE_OPERATIONAL_STATE, + VProps.VITRAGE_TYPE, + ) +messages = [] class TestWebhook(TestActionsBase): @@ -42,6 +69,7 @@ class TestWebhook(TestActionsBase): @classmethod def setUpClass(cls): super(TestWebhook, cls).setUpClass() + cls._template = v_utils.add_template("e2e_webhooks.yaml") # Configure mock server. cls.mock_server_port = _get_free_port() cls.mock_server = MockHTTPServer(('localhost', cls.mock_server_port), @@ -53,6 +81,20 @@ class TestWebhook(TestActionsBase): cls.mock_server_thread.start() cls.URL_PROPS = 'http://localhost:%s/' % cls.mock_server_port + @classmethod + def tearDownClass(cls): + if cls._template is not None: + v_utils.delete_template(cls._template['uuid']) + + def setUp(self): + super(TestWebhook, self).setUp() + + def tearDown(self): + super(TestWebhook, self).tearDown() + del messages[:] + self._delete_webhooks() + self.mock_server.reset_requests_list() + @utils.tempest_logger def test_basic_event(self): @@ -80,9 +122,7 @@ class TestWebhook(TestActionsBase): 'Wrong number of notifications for clear alarm') finally: - self._delete_webhooks() self._trigger_undo_action(TRIGGER_ALARM_1) - self.mock_server.reset_requests_list() @utils.tempest_logger def test_with_no_filter(self): @@ -114,10 +154,8 @@ class TestWebhook(TestActionsBase): 'Wrong number of notifications for clear alarm') finally: - self._delete_webhooks() self._trigger_undo_action(TRIGGER_ALARM_1) self._trigger_undo_action(TRIGGER_ALARM_2) - self.mock_server.reset_requests_list() @utils.tempest_logger def test_with_no_match(self): @@ -146,14 +184,12 @@ class TestWebhook(TestActionsBase): 'event should not have passed filter') finally: - self._delete_webhooks() self._trigger_undo_action(TRIGGER_ALARM_1) self._trigger_undo_action(TRIGGER_ALARM_2) - self.mock_server.reset_requests_list() @utils.tempest_logger def test_multiple_webhooks(self): - """Test to check filter by type and by ID (with 2 different + """Test to check filter by type and with no filter (with 2 separate webhooks) """ @@ -185,46 +221,103 @@ class TestWebhook(TestActionsBase): 'event not posted to all webhooks') finally: - self._delete_webhooks() self._trigger_undo_action(TRIGGER_ALARM_1) self._trigger_undo_action(TRIGGER_ALARM_2) - self.mock_server.reset_requests_list() - # Will be un-commented-out in the next change - # - # @utils.tempest_logger - # def test_webhook_for_deduced_alarm(self): - # - # try: - # - # # Add webhook with filter for the deduced alarm - # TempestClients.vitrage().webhook.add( - # url=self.URL_PROPS, - # regex_filter=NAME_FILTER_FOR_DEDUCED, - # headers=HEADERS_PROPS - # ) - # - # # Raise the trigger alarm - # self._trigger_do_action(TRIGGER_ALARM_WITH_DEDUCED) - # - # # Check event received - expected one for the deduced alarm - # # (the trigger alarm does not pass the filter). This test verifies - # # that the webhook is called only once for the deduced alarm. - # self.assertEqual(1, len(self.mock_server.requests), - # 'Wrong number of notifications for deduced alarm') - # - # # Undo - # self._trigger_undo_action(TRIGGER_ALARM_WITH_DEDUCED) - # - # # Check event undo received - # self.assertEqual(2, len(self.mock_server.requests), - # 'Wrong number of notifications for clear deduced ' - # 'alarm') - # - # finally: - # self._delete_webhooks() - # self._trigger_undo_action(TRIGGER_ALARM_WITH_DEDUCED) - # self.mock_server.reset_requests_list() + @utils.tempest_logger + def test_for_deduced_alarm(self): + + try: + # Add webhook with filter for the deduced alarm + TempestClients.vitrage().webhook.add( + url=self.URL_PROPS, + regex_filter=NAME_FILTER_FOR_DEDUCED, + headers=HEADERS_PROPS + ) + + # Raise the trigger alarm + self._trigger_do_action(TRIGGER_ALARM_WITH_DEDUCED) + + # Check event received - expected one for the deduced alarm + # (the trigger alarm does not pass the filter). This test verifies + # that the webhook is called only once for the deduced alarm. + time.sleep(1) + self.assertEqual(1, len(self.mock_server.requests), + 'Wrong number of notifications for deduced alarm') + + # Undo + self._trigger_undo_action(TRIGGER_ALARM_WITH_DEDUCED) + + # Check event undo received + time.sleep(1) + self.assertEqual(2, len(self.mock_server.requests), + 'Wrong number of notifications for clear deduced ' + 'alarm') + + finally: + self._trigger_undo_action(TRIGGER_ALARM_WITH_DEDUCED) + + @utils.tempest_logger + def test_payload_format(self): + + try: + + TempestClients.vitrage().webhook.add( + url=self.URL_PROPS, + headers=HEADERS_PROPS + ) + + # Raise the trigger alarm + self._trigger_do_action(TRIGGER_ALARM_1) + + # pre check that correct amount of notifications sent + self.assertEqual(1, len(self.mock_server.requests), + 'Wrong number of notifications for alarm') + self.assertEqual(1, len(messages), + 'Wrong number of messages for alarm') + + alarm = ast.literal_eval(messages[0]) + + # check that only specified fields are sent for the alarm, + # payload and resource + passed_filter = utils.filter_data(alarm, MAIN_FILTER, False) + + self.assertEqual(0, len(passed_filter), + "Wrong main fields sent") + + payload = alarm.get(PAYLOAD) + if payload: + passed_filter = utils.filter_data(payload, + DOCTOR_ALARM_FILTER, + False) + + self.assertEqual(0, len(passed_filter), + "Wrong alarm fields sent") + + sent_fields = utils.filter_data(payload, + DOCTOR_ALARM_FILTER, + True) + + self.assertEqual(len(sent_fields), len(DOCTOR_ALARM_FILTER), + "Some alarm fields not sent") + + resource = payload.get(VProps.RESOURCE) + if resource: + passed_filter = utils.filter_data(resource, + RESOURCE_FILTER, + False) + + self.assertEqual(0, len(passed_filter), + "Wrong resource fields sent") + + sent_fields = utils.filter_data(resource, + RESOURCE_FILTER, + True) + + self.assertEqual(len(sent_fields), len(RESOURCE_FILTER), + "Some resource fields not sent") + finally: + self._trigger_undo_action(TRIGGER_ALARM_1) def _delete_webhooks(self): webhooks = TempestClients.vitrage().webhook.list() @@ -233,6 +326,7 @@ class TestWebhook(TestActionsBase): def _get_free_port(): + s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) s.bind(('localhost', 0)) address, port = s.getsockname() @@ -259,8 +353,12 @@ class MockHTTPServer(BaseHTTPServer.HTTPServer): class MockServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_POST(self): - # Process a HTTP Post request and return status code 200 + + content_len = int(self.headers.getheader('content-length', 0)) + # save received JSON + messages.append(str(self.rfile.read(content_len))) + # send fake response self.send_response(requests.codes.ok) self.end_headers() return diff --git a/vitrage_tempest_tests/tests/utils.py b/vitrage_tempest_tests/tests/utils.py index 5768e37..e314064 100644 --- a/vitrage_tempest_tests/tests/utils.py +++ b/vitrage_tempest_tests/tests/utils.py @@ -169,3 +169,9 @@ def wait_for_status(max_waiting, func, **kwargs): time.sleep(2) LOG.error("wait_for_status - False") return False + + +def filter_data(data, filter, match_filter=True): + if match_filter: + return [k for k in data if k in filter] + return [k for k in data if k not in filter]