vitrage-tempest-plugin/vitrage_tempest_plugin/tests/e2e/test_e2e_webhook.py

355 lines
12 KiB
Python

# 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 ast
import http.server as BaseHTTPServer
from oslo_log import log as logging
import requests
import socket
from testtools import matchers
from threading import Thread
import time
from vitrage_tempest_plugin.tests.base import IsEmpty
from vitrage_tempest_plugin.tests.common.constants import VertexProperties as \
VProps
from vitrage_tempest_plugin.tests.common import vitrage_utils as v_utils
from vitrage_tempest_plugin.tests.e2e.test_actions_base import TestActionsBase
from vitrage_tempest_plugin.tests import utils
LOG = logging.getLogger(__name__)
TRIGGER_ALARM_1 = 'e2e.test_webhook.alarm1'
TRIGGER_ALARM_2 = 'e2e.test_webhook.alarm2'
TRIGGER_ALARM_WITH_DEDUCED = 'e2e.test_webhook.alarm_with_deduced'
URL = 'url'
REGEX_FILTER = 'regex_filter'
HEADERS = 'headers'
HEADERS_PROPS = '{"content": "application/json"}'
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.VITRAGE_CATEGORY,
VProps.UPDATE_TIMESTAMP,
VProps.VITRAGE_OPERATIONAL_STATE,
VProps.VITRAGE_TYPE}
messages = []
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),
MockServerRequestHandler)
# Start running mock server in a separate thread.
cls.mock_server_thread = Thread(target=cls.mock_server.serve_forever)
cls.mock_server_thread.setDaemon(True)
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 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):
try:
# Add webhook with filter matching alarm
self.vitrage_client.webhook.add(url=self.URL_PROPS,
regex_filter=NAME_FILTER,
headers=HEADERS_PROPS)
# Raise alarm
self._trigger_do_action(TRIGGER_ALARM_1)
# Check event received
self.assertThat(self.mock_server.requests, matchers.HasLength(1),
'Wrong number of notifications for raise alarm')
# Undo
self._trigger_undo_action(TRIGGER_ALARM_1)
# Check event undo received
self.assertThat(self.mock_server.requests, matchers.HasLength(2),
'Wrong number of notifications for clear alarm')
finally:
self._trigger_undo_action(TRIGGER_ALARM_1)
@utils.tempest_logger
def test_with_no_filter(self):
"""Test to see that a webhook with no filter receives all
notifications
"""
try:
# Add webhook
self.vitrage_client.webhook.add(url=self.URL_PROPS,
regex_filter=NAME_FILTER)
# Raise alarm
self._trigger_do_action(TRIGGER_ALARM_1)
# Check event received
self.assertThat(self.mock_server.requests, matchers.HasLength(1),
'Wrong number of notifications for raise alarm')
# Raise another alarm
self._trigger_do_action(TRIGGER_ALARM_2)
# Check second event received
self.assertThat(self.mock_server.requests, matchers.HasLength(2),
'Wrong number of notifications for clear alarm')
finally:
self._trigger_undo_action(TRIGGER_ALARM_1)
self._trigger_undo_action(TRIGGER_ALARM_2)
@utils.tempest_logger
def test_with_no_match(self):
"""Test to check that filters with no match do not send event """
try:
# Add webhook
self.vitrage_client.webhook.add(url=self.URL_PROPS,
regex_filter=FILTER_NO_MATCH)
# Raise alarm
self._trigger_do_action(TRIGGER_ALARM_1)
# Check event not received
self.assertThat(self.mock_server.requests, IsEmpty(),
'event should not have passed filter')
# Raise another alarm
self._trigger_do_action(TRIGGER_ALARM_2)
# Check second event not received
self.assertThat(self.mock_server.requests, IsEmpty(),
'event should not have passed filter')
finally:
self._trigger_undo_action(TRIGGER_ALARM_1)
self._trigger_undo_action(TRIGGER_ALARM_2)
@utils.tempest_logger
def test_multiple_webhooks(self):
"""Test to check filter by type and with no filter (with 2 separate
webhooks)
"""
try:
# Add webhook
self.vitrage_client.webhook.add(url=self.URL_PROPS,
regex_filter=TYPE_FILTER)
self.vitrage_client.webhook.add(url=self.URL_PROPS)
# Raise alarm
self._trigger_do_action(TRIGGER_ALARM_1)
# Check event received
self.assertThat(self.mock_server.requests, matchers.HasLength(2),
'event not posted to all webhooks')
# Raise another alarm
self._trigger_do_action(TRIGGER_ALARM_2)
# Check second event received
self.assertThat(self.mock_server.requests, matchers.HasLength(4),
'event not posted to all webhooks')
finally:
self._trigger_undo_action(TRIGGER_ALARM_1)
self._trigger_undo_action(TRIGGER_ALARM_2)
@utils.tempest_logger
def test_for_deduced_alarm(self):
try:
# Add webhook with filter for the deduced alarm
self.vitrage_client.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.assertThat(self.mock_server.requests, matchers.HasLength(1),
'Wrong number of notifications for deduced alarm')
# Undo
self._trigger_undo_action(TRIGGER_ALARM_WITH_DEDUCED)
# Check event undo received
time.sleep(1)
self.assertThat(self.mock_server.requests, matchers.HasLength(2),
'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:
self.vitrage_client.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.assertThat(self.mock_server.requests, matchers.HasLength(1),
'Wrong number of notifications for alarm')
self.assertThat(messages, matchers.HasLength(1),
'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,
match_filter=False)
self.assertThat(passed_filter,
IsEmpty(),
"Wrong main fields sent")
payload = alarm.get(PAYLOAD)
if payload:
passed_filter = utils.filter_data(payload,
DOCTOR_ALARM_FILTER,
match_filter=False)
self.assertThat(passed_filter,
IsEmpty(),
"Wrong alarm fields sent")
sent_fields = utils.filter_data(payload, DOCTOR_ALARM_FILTER)
self.assert_set_equal(DOCTOR_ALARM_FILTER, sent_fields,
"Some alarm fields not sent")
resource = payload.get(VProps.RESOURCE)
if resource:
passed_filter = utils.filter_data(resource,
RESOURCE_FILTER,
match_filter=False)
self.assertThat(passed_filter,
IsEmpty(),
"Wrong resource fields sent")
sent_fields = utils.filter_data(resource, RESOURCE_FILTER)
self.assert_set_equal(RESOURCE_FILTER, sent_fields,
"Some resource fields not sent")
finally:
self._trigger_undo_action(TRIGGER_ALARM_1)
@classmethod
def _delete_webhooks(cls):
webhooks = cls.vitrage_client.webhook.list()
for webhook in webhooks:
cls.vitrage_client.webhook.delete(webhook['id'])
def _get_free_port():
s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
s.bind(('localhost', 0))
address, port = s.getsockname()
s.close()
return port
class MockHTTPServer(BaseHTTPServer.HTTPServer):
def __init__(self, server, handler):
BaseHTTPServer.HTTPServer.__init__(self, server, handler)
self.requests = []
def process_request(self, request, client_address):
self.requests.append(request)
LOG.info('received request: %s', request)
BaseHTTPServer.HTTPServer.process_request(
self, client_address=client_address, request=request)
def reset_requests_list(self):
self.requests = []
class MockServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_POST(self):
# Process a HTTP Post request and return status code 200
content_len = int(self.headers.get('content-length', 0))
# save received JSON
messages.append(self.rfile.read(content_len).decode('utf-8'))
# send fake response
self.send_response(requests.codes.ok)
self.end_headers()
return
def log_message(self, format, *args):
pass