add support for webhooks
Implements: blueprint configurable-notifications Change-Id: I0c808c5e44f9d6092d113bb277c8ab8cf0d69716
This commit is contained in:
parent
4ec7fa8ede
commit
274c5b71bf
@ -66,7 +66,7 @@ topics = notifications, vitrage_notifications
|
|||||||
[[post-config|\$VITRAGE_CONF]]
|
[[post-config|\$VITRAGE_CONF]]
|
||||||
|
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
notifiers = mistral,nova
|
notifiers = mistral,nova,webhook
|
||||||
|
|
||||||
[static_physical]
|
[static_physical]
|
||||||
changes_interval = 5
|
changes_interval = 5
|
||||||
|
@ -17,7 +17,8 @@ DEVSTACK_PATH="$BASE/new"
|
|||||||
|
|
||||||
#Argument is received from Zuul
|
#Argument is received from Zuul
|
||||||
if [ "$1" = "api" ]; then
|
if [ "$1" = "api" ]; then
|
||||||
TESTS="topology|test_rca|test_alarms|test_resources|test_template"
|
TESTS="topology|test_rca|test_alarms|test_resources|test_template
|
||||||
|
|test_webhook"
|
||||||
elif [ "$1" = "datasources" ]; then
|
elif [ "$1" = "datasources" ]; then
|
||||||
TESTS="datasources|test_events|notifiers|e2e|database"
|
TESTS="datasources|test_events|notifiers|e2e|database"
|
||||||
else
|
else
|
||||||
|
@ -37,6 +37,7 @@ Notifiers
|
|||||||
nova-notifier
|
nova-notifier
|
||||||
notifier-snmp-plugin
|
notifier-snmp-plugin
|
||||||
mistral-config
|
mistral-config
|
||||||
|
notifier-webhook-plugin
|
||||||
|
|
||||||
|
|
||||||
Machine_Learning
|
Machine_Learning
|
||||||
|
116
doc/source/contributor/notifier-webhook-plugin.rst
Normal file
116
doc/source/contributor/notifier-webhook-plugin.rst
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
=====================
|
||||||
|
Webhook Configuration
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Vitrage can be configured to support webhooks for the sending of
|
||||||
|
notifications regarding raised or cleared alarms to any registered target.
|
||||||
|
|
||||||
|
Enable Webhook Notifier
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
To enable the webhook plugin, add it to the list of notifiers in
|
||||||
|
**/etc/vitrage/vitrage.conf** file:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
notifiers = webhook
|
||||||
|
|
||||||
|
Webhook API
|
||||||
|
===========
|
||||||
|
|
||||||
|
Webhooks can be added, listed and deleted from the database using the
|
||||||
|
following commands:
|
||||||
|
|
||||||
|
Add
|
||||||
|
---
|
||||||
|
To add a new webhook to the database, use the command 'vitrage webhook add'.
|
||||||
|
The fields are:
|
||||||
|
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
| Name | Description | Required |
|
||||||
|
+==================+=================================================================+==============+
|
||||||
|
| url | The webhook URL to which notifications will be sent | Yes |
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
| regex_filter | A JSON string to filter for specific events | No |
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
| headers | A JSON string specifying additional headers to the notification | No |
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
|
||||||
|
|
||||||
|
Usage example::
|
||||||
|
|
||||||
|
vitrage webhook add --url https://www.myserver.com --headers
|
||||||
|
"{'content-type': 'application/json'}" --regex_filter "{'vitrage_type':
|
||||||
|
'.*'}"
|
||||||
|
|
||||||
|
- If no regex filter is supplied, all notifications will be sent.
|
||||||
|
- The defaults headers are : '{'content-type': 'application/json'}'
|
||||||
|
|
||||||
|
Data is sent by the webhook notifier in the following format.
|
||||||
|
|
||||||
|
* notification: ``vitrage.alarm.activate`` or ``vitrage.alarm.deactivate``
|
||||||
|
* payload: The alarm data
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"notification": "vitrage.alarm.activate",
|
||||||
|
"payload": {
|
||||||
|
"vitrage_id": "2def31e9-6d9f-4c16-b007-893caa806cd4",
|
||||||
|
"resource": {
|
||||||
|
"vitrage_id": "437f1f4c-ccce-40a4-ac62-1c2f1fd9f6ac",
|
||||||
|
"name": "app-1-server-1-jz6qvznkmnif",
|
||||||
|
"update_timestamp": "2018-01-22 10:00:34.327142+00:00",
|
||||||
|
"vitrage_category": "RESOURCE",
|
||||||
|
"vitrage_operational_state": "OK",
|
||||||
|
"state": "active",
|
||||||
|
"vitrage_type": "nova.instance",
|
||||||
|
"vitrage_sample_timestamp": "2018-01-22 10:00:34.327142+00:00",
|
||||||
|
"vitrage_aggregated_state": "ACTIVE",
|
||||||
|
"host_id": "iafek-devstack-pre-queens",
|
||||||
|
"project_id": "8f007e5ba0944e84baa6f2a4f2b5d03a",
|
||||||
|
"id": "9b7d93b9-94ec-41e1-9cec-f28d4f8d702c"
|
||||||
|
},
|
||||||
|
"severity": "warning",
|
||||||
|
"update_timestamp": "2018-01-22T10:00:34Z",
|
||||||
|
"resource_id": "437f1f4c-ccce-40a4-ac62-1c2f1fd9f6ac",
|
||||||
|
"vitrage_category": "ALARM",
|
||||||
|
"state": "Active",
|
||||||
|
"vitrage_type": "vitrage",
|
||||||
|
"vitrage_sample_timestamp": "2018-01-22 10:00:34.366364+00:00",
|
||||||
|
"vitrage_operational_severity": "WARNING",
|
||||||
|
"vitrage_aggregated_severity": "WARNING",
|
||||||
|
"vitrage_resource_id": "437f1f4c-ccce-40a4-ac62-1c2f1fd9f6ac",
|
||||||
|
"vitrage_resource_type": "nova.instance",
|
||||||
|
"name": "Instance memory performance degraded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Each of the fields listed can be used to filter the data when specifying a
|
||||||
|
regex filter for the webhook.
|
||||||
|
|
||||||
|
|
||||||
|
List
|
||||||
|
----
|
||||||
|
List all webhooks currently in the DB::
|
||||||
|
|
||||||
|
vitrage webhook list
|
||||||
|
|
||||||
|
Show
|
||||||
|
----
|
||||||
|
Show a webhook with specified id::
|
||||||
|
|
||||||
|
vitrage webhook show <id>
|
||||||
|
|
||||||
|
ID of webhooks is decided by Vitrage and can be found using the 'list' command
|
||||||
|
|
||||||
|
Delete
|
||||||
|
------
|
||||||
|
Delete a webhook with specified id::
|
||||||
|
|
||||||
|
vitrage webhook delete <id>
|
||||||
|
|
||||||
|
ID of webhooks is decided by Vitrage and can be found using the 'list' command
|
@ -1498,7 +1498,7 @@ Resource show
|
|||||||
Show the details of specified resource.
|
Show the details of specified resource.
|
||||||
|
|
||||||
GET /v1/resources/[vitrage_id]
|
GET /v1/resources/[vitrage_id]
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Headers
|
Headers
|
||||||
=======
|
=======
|
||||||
@ -1562,3 +1562,270 @@ Response Examples
|
|||||||
"id": "dc35fa2f-4515-1653-ef6b-03b471bb395b",
|
"id": "dc35fa2f-4515-1653-ef6b-03b471bb395b",
|
||||||
"vitrage_id": "RESOURCE:nova.instance:dc35fa2f-4515-1653-ef6b-03b471bb395b"
|
"vitrage_id": "RESOURCE:nova.instance:dc35fa2f-4515-1653-ef6b-03b471bb395b"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Webhook List
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
List all webhooks.
|
||||||
|
|
||||||
|
GET /v1/webhook/
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Headers
|
||||||
|
=======
|
||||||
|
|
||||||
|
- X-Auth-Token (string, required) - Keystone auth token
|
||||||
|
- Accept (string) - application/json
|
||||||
|
- User-Agent (String)
|
||||||
|
- Content-Type (String): application/json
|
||||||
|
|
||||||
|
Path Parameters
|
||||||
|
===============
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Query Parameters
|
||||||
|
================
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Request Body
|
||||||
|
============
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Request Examples
|
||||||
|
================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /v1/webhook
|
||||||
|
Host: 135.248.18.122:8999
|
||||||
|
User-Agent: keystoneauth1/2.3.0 python-requests/2.9.1 CPython/2.7.6
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/json
|
||||||
|
X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7
|
||||||
|
|
||||||
|
|
||||||
|
Response Status code
|
||||||
|
====================
|
||||||
|
|
||||||
|
- 200 - OK
|
||||||
|
- 404 - Bad request
|
||||||
|
|
||||||
|
Response Body
|
||||||
|
=============
|
||||||
|
|
||||||
|
Returns a list with all webhooks.
|
||||||
|
|
||||||
|
Response Examples
|
||||||
|
=================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"url":"https://requestb.in/tq3fkvtq",
|
||||||
|
"headers":"{'content-type': 'application/json'}",
|
||||||
|
"regex_filter":"{'name':'e2e.*'}",
|
||||||
|
"created_at":"2018-01-04T12:27:47.000000",
|
||||||
|
"id":"c35caf11-f34d-440e-a804-0c1a4fdfb95b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Webhook Show
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
Show the details of specified webhook.
|
||||||
|
|
||||||
|
GET /v1/webhook/[id]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Headers
|
||||||
|
=======
|
||||||
|
|
||||||
|
- X-Auth-Token (string, required) - Keystone auth token
|
||||||
|
- Accept (string) - application/json
|
||||||
|
- User-Agent (String)
|
||||||
|
- Content-Type (String): application/json
|
||||||
|
|
||||||
|
Path Parameters
|
||||||
|
===============
|
||||||
|
|
||||||
|
- id.
|
||||||
|
|
||||||
|
Query Parameters
|
||||||
|
================
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Request Body
|
||||||
|
============
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Request Examples
|
||||||
|
================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
GET /v1/resources/`<id>`
|
||||||
|
Host: 127.0.0.1:8999
|
||||||
|
User-Agent: keystoneauth1/2.3.0 python-requests/2.9.1 CPython/2.7.6
|
||||||
|
Accept: application/json
|
||||||
|
X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7
|
||||||
|
|
||||||
|
Response Status code
|
||||||
|
====================
|
||||||
|
|
||||||
|
- 200 - OK
|
||||||
|
- 404 - Bad request
|
||||||
|
|
||||||
|
Response Body
|
||||||
|
=============
|
||||||
|
|
||||||
|
Returns details of the requested webhook.
|
||||||
|
|
||||||
|
Response Examples
|
||||||
|
=================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"url":"https://requestb.in/tq3fkvtq",
|
||||||
|
"created_at":"2018-01-04T12:27:47.000000",
|
||||||
|
"updated_at":null,
|
||||||
|
"id":"c35caf11-f34d-440e-a804-0c1a4fdfb95b",
|
||||||
|
"headers":"{'content-type': 'application/json'}",
|
||||||
|
"regex_filter":"{'name':'e2e.*'}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Webhook Add
|
||||||
|
^^^^^^^^^^^
|
||||||
|
Add a webhook to the database, to be used by the notifier.
|
||||||
|
|
||||||
|
POST /v1/webhook/
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Headers
|
||||||
|
=======
|
||||||
|
|
||||||
|
- X-Auth-Token (string, required) - Keystone auth token
|
||||||
|
- Accept (string) - application/json
|
||||||
|
- User-Agent (String)
|
||||||
|
- Content-Type (String): application/json
|
||||||
|
|
||||||
|
Path Parameters
|
||||||
|
===============
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Query Parameters
|
||||||
|
================
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Request Body
|
||||||
|
============
|
||||||
|
|
||||||
|
A webhook to be added. Will contain the following fields:
|
||||||
|
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
| Name | Description | Required |
|
||||||
|
+==================+=================================================================+==============+
|
||||||
|
| url | The webhook URL to which notifications will be sent | Yes |
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
| regex_filter | A JSON string to filter for specific events | No |
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
| headers | A JSON string specifying additional headers to the notification | No |
|
||||||
|
+------------------+-----------------------------------------------------------------+--------------+
|
||||||
|
|
||||||
|
- If no regex filter is supplied, all notifications will be sent.
|
||||||
|
- The defaults headers are : '{'content-type': 'application/json'}'
|
||||||
|
|
||||||
|
Request Examples
|
||||||
|
================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
POST /v1/webhook/
|
||||||
|
Host: 135.248.18.122:8999
|
||||||
|
User-Agent: keystoneauth1/2.3.0 python-requests/2.9.1 CPython/2.7.6
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/json
|
||||||
|
X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"webhook":{
|
||||||
|
"url":"https://requestb.in/tqfkvtqa",
|
||||||
|
"headers":null,
|
||||||
|
"regex_filter":"{'name':'e2e.*'}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Response Status code
|
||||||
|
====================
|
||||||
|
|
||||||
|
- 200 - OK
|
||||||
|
- 400 - Bad request
|
||||||
|
|
||||||
|
Response Body
|
||||||
|
=============
|
||||||
|
|
||||||
|
Returns webhook details if request was OK,
|
||||||
|
otherwise returns a detailed error message (e.g. 'headers in bad format').
|
||||||
|
|
||||||
|
Webhook Delete
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
Delete a specified webhook.
|
||||||
|
|
||||||
|
DELETE /v1/webhook/[id]
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Headers
|
||||||
|
=======
|
||||||
|
|
||||||
|
- X-Auth-Token (string, required) - Keystone auth token
|
||||||
|
- Accept (string) - application/json
|
||||||
|
- User-Agent (String)
|
||||||
|
- Content-Type (String): application/json
|
||||||
|
|
||||||
|
Path Parameters
|
||||||
|
===============
|
||||||
|
|
||||||
|
- id.
|
||||||
|
|
||||||
|
Query Parameters
|
||||||
|
================
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Request Body
|
||||||
|
============
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
Request Examples
|
||||||
|
================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
DELETE /v1/resources/`<id>`
|
||||||
|
Host: 127.0.0.1:8999
|
||||||
|
User-Agent: keystoneauth1/2.3.0 python-requests/2.9.1 CPython/2.7.6
|
||||||
|
Accept: application/json
|
||||||
|
X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7
|
||||||
|
|
||||||
|
Response Status code
|
||||||
|
====================
|
||||||
|
|
||||||
|
- 200 - OK
|
||||||
|
- 404 - Bad request
|
||||||
|
|
||||||
|
Response Body
|
||||||
|
=============
|
||||||
|
|
||||||
|
Returns a success message if the webhook is deleted, otherwise an error
|
||||||
|
message is returned.
|
@ -12,9 +12,11 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import pecan
|
import pecan
|
||||||
|
|
||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
from oslo_utils.strutils import bool_from_string
|
from oslo_utils.strutils import bool_from_string
|
||||||
@ -24,6 +26,8 @@ from pecan.core import abort
|
|||||||
from vitrage.api.controllers.rest import RootRestController
|
from vitrage.api.controllers.rest import RootRestController
|
||||||
from vitrage.api.controllers.v1 import count
|
from vitrage.api.controllers.v1 import count
|
||||||
from vitrage.api.policy import enforce
|
from vitrage.api.policy import enforce
|
||||||
|
from vitrage.common.constants import TenantProps
|
||||||
|
from vitrage.common.constants import VertexProperties as Vprops
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -36,8 +40,8 @@ class AlarmsController(RootRestController):
|
|||||||
|
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def get_all(self, **kwargs):
|
def get_all(self, **kwargs):
|
||||||
vitrage_id = kwargs.get('vitrage_id')
|
vitrage_id = kwargs.get(Vprops.VITRAGE_ID)
|
||||||
all_tenants = kwargs.get('all_tenants', False)
|
all_tenants = kwargs.get(TenantProps.ALL_TENANTS, False)
|
||||||
all_tenants = bool_from_string(all_tenants)
|
all_tenants = bool_from_string(all_tenants)
|
||||||
if all_tenants:
|
if all_tenants:
|
||||||
enforce("list alarms:all_tenants", pecan.request.headers,
|
enforce("list alarms:all_tenants", pecan.request.headers,
|
||||||
|
@ -16,6 +16,7 @@ from vitrage.api.controllers.v1 import rca
|
|||||||
from vitrage.api.controllers.v1 import resource
|
from vitrage.api.controllers.v1 import resource
|
||||||
from vitrage.api.controllers.v1 import template
|
from vitrage.api.controllers.v1 import template
|
||||||
from vitrage.api.controllers.v1 import topology
|
from vitrage.api.controllers.v1 import topology
|
||||||
|
from vitrage.api.controllers.v1 import webhook
|
||||||
|
|
||||||
|
|
||||||
class V1Controller(object):
|
class V1Controller(object):
|
||||||
@ -23,5 +24,6 @@ class V1Controller(object):
|
|||||||
resources = resource.ResourcesController()
|
resources = resource.ResourcesController()
|
||||||
alarm = alarm.AlarmsController()
|
alarm = alarm.AlarmsController()
|
||||||
rca = rca.RCAController()
|
rca = rca.RCAController()
|
||||||
|
webhook = webhook.WebhookController()
|
||||||
template = template.TemplateController()
|
template = template.TemplateController()
|
||||||
event = event.EventController()
|
event = event.EventController()
|
||||||
|
141
vitrage/api/controllers/v1/webhook.py
Normal file
141
vitrage/api/controllers/v1/webhook.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# Copyright 2018 - Nokia Corporation
|
||||||
|
#
|
||||||
|
# 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 pecan
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils.strutils import bool_from_string
|
||||||
|
from osprofiler import profiler
|
||||||
|
from pecan.core import abort
|
||||||
|
|
||||||
|
from vitrage.api.controllers.rest import RootRestController
|
||||||
|
from vitrage.api.policy import enforce
|
||||||
|
from vitrage.common.constants import TenantProps
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace_cls("webhook controller",
|
||||||
|
info={}, hide_args=False, trace_private=False)
|
||||||
|
class WebhookController(RootRestController):
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def get_all(self, **kwargs):
|
||||||
|
LOG.info('list all webhooks with args: %s', kwargs)
|
||||||
|
|
||||||
|
all_tenants = kwargs.get(TenantProps.ALL_TENANTS, False)
|
||||||
|
all_tenants = bool_from_string(all_tenants)
|
||||||
|
|
||||||
|
if all_tenants:
|
||||||
|
enforce('webhook list:all_tenants', pecan.request.headers,
|
||||||
|
pecan.request.enforcer, {})
|
||||||
|
else:
|
||||||
|
enforce('webhook list', pecan.request.headers,
|
||||||
|
pecan.request.enforcer, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._get_all(all_tenants)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('failed to list webhooks %s', e)
|
||||||
|
abort(404, str(e))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_all(all_tenants):
|
||||||
|
webhooks = \
|
||||||
|
pecan.request.client.call(pecan.request.context,
|
||||||
|
'get_all_webhooks',
|
||||||
|
all_tenants=all_tenants)
|
||||||
|
LOG.info(webhooks)
|
||||||
|
return webhooks
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def get(self, id):
|
||||||
|
LOG.info('Show webhook with id: %s', id)
|
||||||
|
|
||||||
|
enforce('webhook show', pecan.request.headers,
|
||||||
|
pecan.request.enforcer, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._get(id)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Failed to get webhooks %s', e)
|
||||||
|
abort(404, str(e))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get(id):
|
||||||
|
webhook = \
|
||||||
|
pecan.request.client.call(pecan.request.context,
|
||||||
|
'get_webhook',
|
||||||
|
id=id)
|
||||||
|
LOG.info(webhook)
|
||||||
|
if not webhook:
|
||||||
|
abort(404, "Failed to find webhook with ID: %s" % id)
|
||||||
|
return webhook
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def post(self, **kwargs):
|
||||||
|
LOG.info("Add webhook with following props: %s" % str(
|
||||||
|
kwargs))
|
||||||
|
enforce('webhook add', pecan.request.headers,
|
||||||
|
pecan.request.enforcer, {})
|
||||||
|
try:
|
||||||
|
return self._post(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Failed to add webhooks %s', e)
|
||||||
|
abort(400, str(e))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _post(**kwargs):
|
||||||
|
url = kwargs.get('url')
|
||||||
|
if not url:
|
||||||
|
abort(400, 'Missing mandatory field: URL')
|
||||||
|
regex_filter = kwargs.get('regex_filter', None)
|
||||||
|
headers = kwargs.get('headers', None)
|
||||||
|
|
||||||
|
webhook = \
|
||||||
|
pecan.request.client.call(pecan.request.context,
|
||||||
|
'add_webhook',
|
||||||
|
url=url,
|
||||||
|
regex_filter=regex_filter,
|
||||||
|
headers=headers)
|
||||||
|
LOG.info(webhook)
|
||||||
|
if webhook.get("ERROR"):
|
||||||
|
abort(400, "Failed to add webhook: %s" % webhook.get("ERROR"))
|
||||||
|
return webhook
|
||||||
|
|
||||||
|
@pecan.expose('json')
|
||||||
|
def delete(self, id):
|
||||||
|
|
||||||
|
LOG.info('delete webhook with id: %s', id)
|
||||||
|
enforce("webhook delete",
|
||||||
|
pecan.request.headers,
|
||||||
|
pecan.request.enforcer,
|
||||||
|
{})
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._delete_registration(id)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Failed to delete webhook %s: '
|
||||||
|
'%s' % (id, str(e)))
|
||||||
|
abort(404, str(e))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _delete_registration(id):
|
||||||
|
resource = pecan.request.client.call(
|
||||||
|
pecan.request.context,
|
||||||
|
'delete_webhook',
|
||||||
|
id=id)
|
||||||
|
if not resource:
|
||||||
|
abort(404, "Failed to find resource with ID: %s" % id)
|
||||||
|
LOG.info("Request returned with: %s" % resource)
|
||||||
|
return resource
|
@ -20,6 +20,7 @@ from vitrage.api_handler.apis.base import ALARM_QUERY
|
|||||||
from vitrage.api_handler.apis.base import ALARMS_ALL_QUERY
|
from vitrage.api_handler.apis.base import ALARMS_ALL_QUERY
|
||||||
from vitrage.api_handler.apis.base import EntityGraphApisBase
|
from vitrage.api_handler.apis.base import EntityGraphApisBase
|
||||||
from vitrage.common.constants import EntityCategory as ECategory
|
from vitrage.common.constants import EntityCategory as ECategory
|
||||||
|
from vitrage.common.constants import TenantProps
|
||||||
from vitrage.common.constants import VertexProperties as VProps
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
from vitrage.entity_graph.mappings.operational_alarm_severity import \
|
from vitrage.entity_graph.mappings.operational_alarm_severity import \
|
||||||
OperationalAlarmSeverity
|
OperationalAlarmSeverity
|
||||||
@ -40,8 +41,8 @@ class AlarmApis(EntityGraphApisBase):
|
|||||||
LOG.debug("AlarmApis get_alarms - vitrage_id: %s, all_tenants=%s",
|
LOG.debug("AlarmApis get_alarms - vitrage_id: %s, all_tenants=%s",
|
||||||
str(vitrage_id), all_tenants)
|
str(vitrage_id), all_tenants)
|
||||||
|
|
||||||
project_id = ctx.get(self.TENANT_PROPERTY, None)
|
project_id = ctx.get(TenantProps.TENANT, None)
|
||||||
is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False)
|
is_admin_project = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
|
|
||||||
if not vitrage_id or vitrage_id == 'all':
|
if not vitrage_id or vitrage_id == 'all':
|
||||||
if all_tenants:
|
if all_tenants:
|
||||||
@ -68,8 +69,8 @@ class AlarmApis(EntityGraphApisBase):
|
|||||||
LOG.warning('Alarm show - Not found (%s)', vitrage_id)
|
LOG.warning('Alarm show - Not found (%s)', vitrage_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
is_admin = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False)
|
is_admin = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
curr_project = ctx.get(self.TENANT_PROPERTY, None)
|
curr_project = ctx.get(TenantProps.TENANT, None)
|
||||||
alarm_project = alarm.get(VProps.PROJECT_ID)
|
alarm_project = alarm.get(VProps.PROJECT_ID)
|
||||||
if not is_admin and curr_project != alarm_project:
|
if not is_admin and curr_project != alarm_project:
|
||||||
LOG.warning('Alarm show - Authorization failed (%s)', vitrage_id)
|
LOG.warning('Alarm show - Authorization failed (%s)', vitrage_id)
|
||||||
@ -80,8 +81,8 @@ class AlarmApis(EntityGraphApisBase):
|
|||||||
def get_alarm_counts(self, ctx, all_tenants):
|
def get_alarm_counts(self, ctx, all_tenants):
|
||||||
LOG.debug("AlarmApis get_alarm_counts - all_tenants=%s", all_tenants)
|
LOG.debug("AlarmApis get_alarm_counts - all_tenants=%s", all_tenants)
|
||||||
|
|
||||||
project_id = ctx.get(self.TENANT_PROPERTY, None)
|
project_id = ctx.get(TenantProps.TENANT, None)
|
||||||
is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False)
|
is_admin_project = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
|
|
||||||
if all_tenants:
|
if all_tenants:
|
||||||
alarms = self.entity_graph.get_vertices(
|
alarms = self.entity_graph.get_vertices(
|
||||||
|
@ -89,8 +89,6 @@ RESOURCES_ALL_QUERY = {
|
|||||||
|
|
||||||
|
|
||||||
class EntityGraphApisBase(object):
|
class EntityGraphApisBase(object):
|
||||||
TENANT_PROPERTY = 'tenant'
|
|
||||||
IS_ADMIN_PROJECT_PROPERTY = 'is_admin'
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_query_with_project(vitrage_category, project_id, is_admin):
|
def _get_query_with_project(vitrage_category, project_id, is_admin):
|
||||||
|
@ -15,10 +15,12 @@
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from osprofiler import profiler
|
from osprofiler import profiler
|
||||||
|
|
||||||
|
|
||||||
from vitrage.api_handler.apis.base import ALARMS_ALL_QUERY
|
from vitrage.api_handler.apis.base import ALARMS_ALL_QUERY
|
||||||
from vitrage.api_handler.apis.base import EDGE_QUERY
|
from vitrage.api_handler.apis.base import EDGE_QUERY
|
||||||
from vitrage.api_handler.apis.base import EntityGraphApisBase
|
from vitrage.api_handler.apis.base import EntityGraphApisBase
|
||||||
from vitrage.api_handler.apis.base import RCA_QUERY
|
from vitrage.api_handler.apis.base import RCA_QUERY
|
||||||
|
from vitrage.common.constants import TenantProps
|
||||||
from vitrage.graph import Direction
|
from vitrage.graph import Direction
|
||||||
|
|
||||||
|
|
||||||
@ -37,8 +39,8 @@ class RcaApis(EntityGraphApisBase):
|
|||||||
LOG.debug("RcaApis get_rca - root: %s, all_tenants=%s",
|
LOG.debug("RcaApis get_rca - root: %s, all_tenants=%s",
|
||||||
str(root), all_tenants)
|
str(root), all_tenants)
|
||||||
|
|
||||||
project_id = ctx.get(self.TENANT_PROPERTY, None)
|
project_id = ctx.get(TenantProps.TENANT, None)
|
||||||
is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False)
|
is_admin_project = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
ga = self.entity_graph.algo
|
ga = self.entity_graph.algo
|
||||||
|
|
||||||
found_graph_out = ga.graph_query_vertices(root,
|
found_graph_out = ga.graph_query_vertices(root,
|
||||||
|
@ -19,6 +19,7 @@ from osprofiler import profiler
|
|||||||
from vitrage.api_handler.apis.base import EntityGraphApisBase
|
from vitrage.api_handler.apis.base import EntityGraphApisBase
|
||||||
from vitrage.api_handler.apis.base import RESOURCES_ALL_QUERY
|
from vitrage.api_handler.apis.base import RESOURCES_ALL_QUERY
|
||||||
from vitrage.common.constants import EntityCategory
|
from vitrage.common.constants import EntityCategory
|
||||||
|
from vitrage.common.constants import TenantProps
|
||||||
from vitrage.common.constants import VertexProperties as VProps
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
|
||||||
|
|
||||||
@ -37,8 +38,8 @@ class ResourceApis(EntityGraphApisBase):
|
|||||||
LOG.debug('ResourceApis get_resources - resource_type: %s,'
|
LOG.debug('ResourceApis get_resources - resource_type: %s,'
|
||||||
'all_tenants: %s', str(resource_type), all_tenants)
|
'all_tenants: %s', str(resource_type), all_tenants)
|
||||||
|
|
||||||
project_id = ctx.get(self.TENANT_PROPERTY, None)
|
project_id = ctx.get(TenantProps.TENANT, None)
|
||||||
is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False)
|
is_admin_project = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
|
|
||||||
if all_tenants:
|
if all_tenants:
|
||||||
resource_query = RESOURCES_ALL_QUERY
|
resource_query = RESOURCES_ALL_QUERY
|
||||||
@ -66,8 +67,8 @@ class ResourceApis(EntityGraphApisBase):
|
|||||||
LOG.warning('Resource show - Not found (%s)', vitrage_id)
|
LOG.warning('Resource show - Not found (%s)', vitrage_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
is_admin = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False)
|
is_admin = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
curr_project = ctx.get(self.TENANT_PROPERTY, None)
|
curr_project = ctx.get(TenantProps.TENANT, None)
|
||||||
resource_project = resource.get(VProps.PROJECT_ID)
|
resource_project = resource.get(VProps.PROJECT_ID)
|
||||||
if not is_admin and curr_project != resource_project:
|
if not is_admin and curr_project != resource_project:
|
||||||
LOG.warning('Resource show - Authorization failed (%s)',
|
LOG.warning('Resource show - Authorization failed (%s)',
|
||||||
|
@ -21,6 +21,7 @@ from vitrage.api_handler.apis.base import EntityGraphApisBase
|
|||||||
from vitrage.api_handler.apis.base import TOPOLOGY_AND_ALARMS_QUERY
|
from vitrage.api_handler.apis.base import TOPOLOGY_AND_ALARMS_QUERY
|
||||||
from vitrage.common.constants import EdgeProperties as EProps
|
from vitrage.common.constants import EdgeProperties as EProps
|
||||||
from vitrage.common.constants import EntityCategory
|
from vitrage.common.constants import EntityCategory
|
||||||
|
from vitrage.common.constants import TenantProps
|
||||||
from vitrage.common.constants import VertexProperties as VProps
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
from vitrage.common.exception import VitrageError
|
from vitrage.common.exception import VitrageError
|
||||||
from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE
|
from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE
|
||||||
@ -41,8 +42,8 @@ class TopologyApis(EntityGraphApisBase):
|
|||||||
LOG.debug("TopologyApis get_topology - root: %s, all_tenants=%s",
|
LOG.debug("TopologyApis get_topology - root: %s, all_tenants=%s",
|
||||||
str(root), all_tenants)
|
str(root), all_tenants)
|
||||||
|
|
||||||
project_id = ctx.get(self.TENANT_PROPERTY, None)
|
project_id = ctx.get(TenantProps.TENANT, None)
|
||||||
is_admin_project = ctx.get(self.IS_ADMIN_PROJECT_PROPERTY, False)
|
is_admin_project = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
ga = self.entity_graph.algo
|
ga = self.entity_graph.algo
|
||||||
|
|
||||||
LOG.debug('project_id = %s, is_admin_project %s',
|
LOG.debug('project_id = %s, is_admin_project %s',
|
||||||
|
153
vitrage/api_handler/apis/webhook.py
Normal file
153
vitrage/api_handler/apis/webhook.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# Copyright 2018 - 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
|
||||||
|
from collections import namedtuple
|
||||||
|
import datetime
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
from osprofiler import profiler
|
||||||
|
import re
|
||||||
|
from six.moves.urllib.parse import urlparse
|
||||||
|
from vitrage.common.constants import TenantProps
|
||||||
|
from vitrage.common.constants import VertexProperties as Vprops
|
||||||
|
from vitrage.notifier.plugins.webhook.utils import db_row_to_dict
|
||||||
|
from vitrage import storage
|
||||||
|
from vitrage.storage.sqlalchemy.models import Webhooks
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
Result = namedtuple("Result", ["is_valid", "message"])
|
||||||
|
|
||||||
|
|
||||||
|
@profiler.trace_cls("webhook apis",
|
||||||
|
info={}, hide_args=False, trace_private=False)
|
||||||
|
class WebhookApis(object):
|
||||||
|
DELETED_ROWS_SUCCESS = 1
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
self.conf = conf
|
||||||
|
self.db_conn = storage.get_connection_from_config(conf)
|
||||||
|
|
||||||
|
def delete_webhook(self, ctx, id):
|
||||||
|
|
||||||
|
LOG.info("Delete webhook with id: %s",
|
||||||
|
str(id))
|
||||||
|
|
||||||
|
deleted_rows_count = self.db_conn.webhooks.delete(id)
|
||||||
|
|
||||||
|
if deleted_rows_count == self.DELETED_ROWS_SUCCESS:
|
||||||
|
return {'SUCCESS': 'Webhook %s deleted' % id}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_webhooks(self, ctx, all_tenants):
|
||||||
|
LOG.info("List all webhooks")
|
||||||
|
if all_tenants and ctx.get(TenantProps.IS_ADMIN, False):
|
||||||
|
res = self.db_conn.webhooks.query()
|
||||||
|
else:
|
||||||
|
res = self.db_conn.webhooks.query(project_id=ctx.get(
|
||||||
|
TenantProps.TENANT, ""))
|
||||||
|
LOG.info(res)
|
||||||
|
webhooks = [db_row_to_dict(webhook) for webhook in res]
|
||||||
|
|
||||||
|
return webhooks
|
||||||
|
|
||||||
|
def add_webhook(self, ctx, url, headers=None, regex_filter=None):
|
||||||
|
res = self._check_valid_webhook(url, headers, regex_filter)
|
||||||
|
if not res.is_valid:
|
||||||
|
LOG.exception("Failed to create webhook: %s" % res.message)
|
||||||
|
return res.message
|
||||||
|
try:
|
||||||
|
db_row = self._webhook_to_db_row(url, headers, regex_filter, ctx)
|
||||||
|
self.db_conn.webhooks.create(db_row)
|
||||||
|
return db_row_to_dict(db_row)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("Failed to add webhook to DB: %s", str(e))
|
||||||
|
return {"ERROR": str(e)}
|
||||||
|
|
||||||
|
def get_webhook(self, ctx, id):
|
||||||
|
try:
|
||||||
|
webhooks = self.db_conn.webhooks.query(id=id)
|
||||||
|
# Check that webhook belongs to current tenant or current tenant
|
||||||
|
# is admin
|
||||||
|
if len(webhooks) == 0:
|
||||||
|
LOG.warning("Webhook not found - %s" % id)
|
||||||
|
return None
|
||||||
|
if ctx.get(TenantProps.TENANT, "") == \
|
||||||
|
webhooks[0][Vprops.PROJECT_ID] or ctx.get(
|
||||||
|
TenantProps.IS_ADMIN, False):
|
||||||
|
return (webhooks[0])
|
||||||
|
else:
|
||||||
|
LOG.warning('Webhook show - Authorization failed (%s)',
|
||||||
|
id)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("Failed to get webhook: %s", str(e))
|
||||||
|
return {"ERROR": str(e)}
|
||||||
|
|
||||||
|
def _webhook_to_db_row(self, url, headers, regex_filter, ctx):
|
||||||
|
if not regex_filter:
|
||||||
|
regex_filter = ""
|
||||||
|
if not headers:
|
||||||
|
headers = ""
|
||||||
|
uuid = uuidutils.generate_uuid()
|
||||||
|
project_id = ctx.get(TenantProps.TENANT, "")
|
||||||
|
is_admin = ctx.get(TenantProps.IS_ADMIN, False)
|
||||||
|
created_at = str(datetime.datetime.now())
|
||||||
|
db_row = Webhooks(id=uuid,
|
||||||
|
project_id=project_id,
|
||||||
|
is_admin_webhook=is_admin,
|
||||||
|
created_at=created_at,
|
||||||
|
url=url,
|
||||||
|
headers=headers,
|
||||||
|
regex_filter=regex_filter)
|
||||||
|
return db_row
|
||||||
|
|
||||||
|
def _check_valid_webhook(self, url, headers, regex_filter):
|
||||||
|
if not self._validate_url(url):
|
||||||
|
return Result(False, {"ERROR": "Invalid URL"})
|
||||||
|
elif not self._validate_headers(headers):
|
||||||
|
return Result(False, {"ERROR": "Headers in invalid format"})
|
||||||
|
elif not self._validate_regex(regex_filter):
|
||||||
|
return Result(False, {"ERROR": "Invalid RegEx"})
|
||||||
|
return Result(True, "")
|
||||||
|
|
||||||
|
def _validate_url(self, url):
|
||||||
|
try:
|
||||||
|
result = urlparse(url)
|
||||||
|
if not result.scheme or not result.netloc:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_regex(self, regex_filter):
|
||||||
|
if regex_filter:
|
||||||
|
try:
|
||||||
|
filter_dict = ast.literal_eval(regex_filter)
|
||||||
|
if not isinstance(filter_dict, dict):
|
||||||
|
return False
|
||||||
|
for filter in filter_dict.values():
|
||||||
|
re.compile(filter)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_headers(self, headers):
|
||||||
|
if headers:
|
||||||
|
try:
|
||||||
|
return isinstance(ast.literal_eval(headers), dict)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
@ -25,6 +25,7 @@ from vitrage.api_handler.apis.rca import RcaApis
|
|||||||
from vitrage.api_handler.apis.resource import ResourceApis
|
from vitrage.api_handler.apis.resource import ResourceApis
|
||||||
from vitrage.api_handler.apis.template import TemplateApis
|
from vitrage.api_handler.apis.template import TemplateApis
|
||||||
from vitrage.api_handler.apis.topology import TopologyApis
|
from vitrage.api_handler.apis.topology import TopologyApis
|
||||||
|
from vitrage.api_handler.apis.webhook import WebhookApis
|
||||||
from vitrage import messaging
|
from vitrage import messaging
|
||||||
from vitrage import rpc as vitrage_rpc
|
from vitrage import rpc as vitrage_rpc
|
||||||
|
|
||||||
@ -55,7 +56,8 @@ class VitrageApiHandlerService(os_service.Service):
|
|||||||
RcaApis(self.entity_graph, self.conf),
|
RcaApis(self.entity_graph, self.conf),
|
||||||
TemplateApis(self.notifier),
|
TemplateApis(self.notifier),
|
||||||
EventApis(self.conf),
|
EventApis(self.conf),
|
||||||
ResourceApis(self.entity_graph, self.conf)]
|
ResourceApis(self.entity_graph, self.conf),
|
||||||
|
WebhookApis(self.conf)]
|
||||||
|
|
||||||
server = vitrage_rpc.get_server(target, endpoints, transport)
|
server = vitrage_rpc.get_server(target, endpoints, transport)
|
||||||
|
|
||||||
|
@ -169,3 +169,9 @@ class TemplateStatus(object):
|
|||||||
DELETING = 'DELETING'
|
DELETING = 'DELETING'
|
||||||
DELETED = 'DELETED'
|
DELETED = 'DELETED'
|
||||||
LOADING = 'LOADING'
|
LOADING = 'LOADING'
|
||||||
|
|
||||||
|
|
||||||
|
class TenantProps(object):
|
||||||
|
ALL_TENANTS = 'all_tenants'
|
||||||
|
TENANT = 'tenant'
|
||||||
|
IS_ADMIN = 'is_admin'
|
||||||
|
@ -18,6 +18,7 @@ from vitrage.common.policies import rca
|
|||||||
from vitrage.common.policies import resource
|
from vitrage.common.policies import resource
|
||||||
from vitrage.common.policies import template
|
from vitrage.common.policies import template
|
||||||
from vitrage.common.policies import topology
|
from vitrage.common.policies import topology
|
||||||
|
from vitrage.common.policies import webhook
|
||||||
|
|
||||||
|
|
||||||
def list_rules():
|
def list_rules():
|
||||||
@ -27,5 +28,6 @@ def list_rules():
|
|||||||
rca.list_rules(),
|
rca.list_rules(),
|
||||||
template.list_rules(),
|
template.list_rules(),
|
||||||
topology.list_rules(),
|
topology.list_rules(),
|
||||||
resource.list_rules()
|
resource.list_rules(),
|
||||||
|
webhook.list_rules()
|
||||||
)
|
)
|
||||||
|
82
vitrage/common/policies/webhook.py
Normal file
82
vitrage/common/policies/webhook.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Copyright 2018 - Nokia Corporation
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_policy import policy
|
||||||
|
|
||||||
|
from vitrage.common.policies import base
|
||||||
|
|
||||||
|
webhook = 'webhook %s'
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=webhook % 'delete',
|
||||||
|
check_str=base.UNPROTECTED,
|
||||||
|
description='Delete a webhook registration',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/webhook/{webhook_id}',
|
||||||
|
'method': 'DELETE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=webhook % 'list',
|
||||||
|
check_str=base.UNPROTECTED,
|
||||||
|
description='List all webhook registrations',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/webhook',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=webhook % 'list:all_tenants',
|
||||||
|
check_str=base.ROLE_ADMIN,
|
||||||
|
description='List all webhooks (if the user'
|
||||||
|
' has the permissions)',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/webhook',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=webhook % 'show',
|
||||||
|
check_str=base.UNPROTECTED,
|
||||||
|
description='Show a webhook registration with a given id',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/webhook/{webhook_id}',
|
||||||
|
'method': 'GET'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=webhook % 'add',
|
||||||
|
check_str=base.UNPROTECTED,
|
||||||
|
description='Add a webhook registration with given info',
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'path': '/webhook',
|
||||||
|
'method': 'POST'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return rules
|
27
vitrage/notifier/plugins/webhook/__init__.py
Normal file
27
vitrage/notifier/plugins/webhook/__init__.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Copyright 2018 - Nokia Corporation
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
OPTS = [
|
||||||
|
cfg.StrOpt('notifier',
|
||||||
|
default='vitrage.notifier.plugins.webhook.'
|
||||||
|
'webhook.Webhook',
|
||||||
|
help='notifier webhook class path',
|
||||||
|
required=True),
|
||||||
|
cfg.IntOpt('max_retries',
|
||||||
|
default=2,
|
||||||
|
help='rest http post max retries',
|
||||||
|
required=False),
|
||||||
|
]
|
26
vitrage/notifier/plugins/webhook/utils.py
Normal file
26
vitrage/notifier/plugins/webhook/utils.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Copyright 2018 - Nokia Corporation
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
def db_row_to_dict(row):
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': row.id,
|
||||||
|
'project_id': row.project_id,
|
||||||
|
'is_admin_webhook': row.is_admin_webhook,
|
||||||
|
'created_at': row.created_at,
|
||||||
|
'url': row.url,
|
||||||
|
'regex_filter': row.regex_filter,
|
||||||
|
'headers': row.headers
|
||||||
|
}
|
139
vitrage/notifier/plugins/webhook/webhook.py
Normal file
139
vitrage/notifier/plugins/webhook/webhook.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# 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 re
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from vitrage.common.constants import NotifierEventTypes
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage.notifier.plugins.base import NotifierBase
|
||||||
|
from vitrage.notifier.plugins.webhook import utils as webhook_utils
|
||||||
|
from vitrage import storage
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
URL = 'url'
|
||||||
|
IS_ADMIN_WEBHOOK = 'is_admin_webhook'
|
||||||
|
FIELDS_TO_REMOVE = (VProps.VITRAGE_IS_PLACEHOLDER,
|
||||||
|
VProps.VITRAGE_IS_DELETED,
|
||||||
|
VProps.IS_REAL_VITRAGE_ID)
|
||||||
|
NOTIFICATION_TYPE = 'notification_type'
|
||||||
|
PAYLOAD = 'payload'
|
||||||
|
|
||||||
|
|
||||||
|
class Webhook(NotifierBase):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_notifier_name():
|
||||||
|
return 'webhook'
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
super(Webhook, self).__init__(conf)
|
||||||
|
self.conf = conf
|
||||||
|
self._db = storage.get_connection_from_config(self.conf)
|
||||||
|
self.max_retries = self.conf.webhook.max_retries
|
||||||
|
self.default_headers = {'content-type': 'application/json'}
|
||||||
|
|
||||||
|
def process_event(self, data, event_type):
|
||||||
|
|
||||||
|
if event_type == NotifierEventTypes.ACTIVATE_ALARM_EVENT \
|
||||||
|
or event_type == NotifierEventTypes.DEACTIVATE_ALARM_EVENT:
|
||||||
|
|
||||||
|
LOG.info('Webhook API starting to process %s', str(data))
|
||||||
|
|
||||||
|
webhooks = self._load_webhooks()
|
||||||
|
if webhooks:
|
||||||
|
for webhook in webhooks:
|
||||||
|
webhook_filters = self._get_webhook_filters(webhook)
|
||||||
|
data = self._filter_fields(data)
|
||||||
|
if self._check_against_filter(webhook_filters, data)\
|
||||||
|
and self._check_correct_tenant(webhook, data):
|
||||||
|
self._post_data(webhook, event_type, data)
|
||||||
|
|
||||||
|
LOG.info('Webhook API finished processing %s', str(data))
|
||||||
|
|
||||||
|
def _post_data(self, webhook, event_type, data):
|
||||||
|
try:
|
||||||
|
webhook_data = {'notification': event_type, 'payload': data}
|
||||||
|
webhook_headers = self._get_webhook_headers(webhook)
|
||||||
|
session = requests.Session()
|
||||||
|
session.mount(str(webhook[URL]),
|
||||||
|
requests.adapters.HTTPAdapter(
|
||||||
|
max_retries=self.max_retries))
|
||||||
|
resp = session.post(str(webhook[URL]),
|
||||||
|
data=jsonutils.dumps(webhook_data),
|
||||||
|
headers=webhook_headers)
|
||||||
|
LOG.info('posted %s to %s. Response status %s, reason %s',
|
||||||
|
str(webhook_data), str(webhook[URL]),
|
||||||
|
resp.status_code, resp.reason)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("Could not post to webhook %s %s" % (
|
||||||
|
str(webhook['id']), str(e)))
|
||||||
|
|
||||||
|
def _load_webhooks(self):
|
||||||
|
db_webhooks = self._db.webhooks.query()
|
||||||
|
return [webhook_utils.db_row_to_dict(webhook) for webhook in
|
||||||
|
db_webhooks]
|
||||||
|
|
||||||
|
def _get_webhook_headers(self, webhook):
|
||||||
|
headers = self.default_headers.copy()
|
||||||
|
headers['x-openstack-request-id'] = b'req-' + \
|
||||||
|
uuidutils.generate_uuid().encode(
|
||||||
|
'ascii')
|
||||||
|
if webhook.get('headers') != '':
|
||||||
|
headers.update(ast.literal_eval(webhook['headers']))
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _get_webhook_filters(self, webhook):
|
||||||
|
filters = webhook.get('regex_filter')
|
||||||
|
if filters != '':
|
||||||
|
filters = ast.literal_eval(filters)
|
||||||
|
for k, v in filters.items():
|
||||||
|
filters[k] = re.compile(v, re.IGNORECASE)
|
||||||
|
return filters
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _check_against_filter(self, webhook_filters, event):
|
||||||
|
# Check if the event matches the specified filters
|
||||||
|
if webhook_filters:
|
||||||
|
for field, filter in webhook_filters.items():
|
||||||
|
value = event.get(field)
|
||||||
|
if value is None:
|
||||||
|
return False
|
||||||
|
elif filter.match(value) is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _filter_fields(self, data):
|
||||||
|
data = {k: v for k, v in data.items() if k not in FIELDS_TO_REMOVE}
|
||||||
|
if data.get(VProps.RESOURCE):
|
||||||
|
data[VProps.RESOURCE] = \
|
||||||
|
self._filter_fields(data[VProps.RESOURCE])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _check_correct_tenant(self, webhook, data):
|
||||||
|
# Check that the resource project ID matches the project ID under
|
||||||
|
# which the webhook was added.
|
||||||
|
|
||||||
|
if webhook.get(IS_ADMIN_WEBHOOK):
|
||||||
|
return True
|
||||||
|
if data.get(VProps.RESOURCE):
|
||||||
|
if data[VProps.RESOURCE].get(VProps.PROJECT_ID):
|
||||||
|
return data[VProps.RESOURCE][VProps.PROJECT_ID] == \
|
||||||
|
webhook.get(VProps.PROJECT_ID)
|
||||||
|
return True
|
@ -27,6 +27,7 @@ import vitrage.machine_learning
|
|||||||
import vitrage.machine_learning.plugins.jaccard_correlation
|
import vitrage.machine_learning.plugins.jaccard_correlation
|
||||||
import vitrage.notifier
|
import vitrage.notifier
|
||||||
import vitrage.notifier.plugins.snmp
|
import vitrage.notifier.plugins.snmp
|
||||||
|
import vitrage.notifier.plugins.webhook
|
||||||
import vitrage.os_clients
|
import vitrage.os_clients
|
||||||
import vitrage.persistency
|
import vitrage.persistency
|
||||||
import vitrage.rpc
|
import vitrage.rpc
|
||||||
@ -56,6 +57,7 @@ def list_opts():
|
|||||||
('jaccard_correlation',
|
('jaccard_correlation',
|
||||||
vitrage.machine_learning.plugins.jaccard_correlation.OPTS),
|
vitrage.machine_learning.plugins.jaccard_correlation.OPTS),
|
||||||
('snmp', vitrage.notifier.plugins.snmp.OPTS),
|
('snmp', vitrage.notifier.plugins.snmp.OPTS),
|
||||||
|
('webhook', vitrage.notifier.plugins.webhook.OPTS),
|
||||||
('snmp_parsing', vitrage.snmp_parsing.OPTS),
|
('snmp_parsing', vitrage.snmp_parsing.OPTS),
|
||||||
('DEFAULT', itertools.chain(
|
('DEFAULT', itertools.chain(
|
||||||
vitrage.os_clients.OPTS,
|
vitrage.os_clients.OPTS,
|
||||||
@ -98,6 +100,8 @@ def _normalize_path_to_datasource_name(path_list, top=os.getcwd()):
|
|||||||
def register_opts(conf, package_name, paths):
|
def register_opts(conf, package_name, paths):
|
||||||
"""register opts of package package_name, with base path in paths"""
|
"""register opts of package package_name, with base path in paths"""
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
LOG.info("package name: %s" % package_name)
|
||||||
|
LOG.info("path: % s" % path)
|
||||||
try:
|
try:
|
||||||
opt = importutils.import_module(
|
opt = importutils.import_module(
|
||||||
"%s.%s" % (path, package_name)).OPTS
|
"%s.%s" % (path, package_name)).OPTS
|
||||||
@ -107,6 +111,5 @@ def register_opts(conf, package_name, paths):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
LOG.error("Failed to register config options for %s" %
|
||||||
|
package_name)
|
||||||
LOG.error("Failed to register config options for %s" % package_name)
|
|
||||||
|
@ -39,17 +39,21 @@ class Connection(object):
|
|||||||
def graph_snapshots(self):
|
def graph_snapshots(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def webhooks(self):
|
||||||
|
return None
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def upgrade(self, nocreate=False):
|
def upgrade(self, nocreate=False):
|
||||||
raise NotImplementedError('upgrade not implemented')
|
raise NotImplementedError('upgrade is not implemented')
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
raise NotImplementedError('disconnect not implemented')
|
raise NotImplementedError('disconnect is not implemented')
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def clear(self):
|
def clear(self):
|
||||||
raise NotImplementedError('clear not implemented')
|
raise NotImplementedError('clear is not implemented')
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
@ -60,7 +64,7 @@ class ActiveActionsConnection(object):
|
|||||||
|
|
||||||
:type active_action: vitrage.storage.sqlalchemy.models.ActiveAction
|
:type active_action: vitrage.storage.sqlalchemy.models.ActiveAction
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('create active action not implemented')
|
raise NotImplementedError('create active action is not implemented')
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def update(self, active_action):
|
def update(self, active_action):
|
||||||
@ -68,7 +72,7 @@ class ActiveActionsConnection(object):
|
|||||||
|
|
||||||
:type active_action: vitrage.storage.sqlalchemy.models.ActiveAction
|
:type active_action: vitrage.storage.sqlalchemy.models.ActiveAction
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('update active action not implemented')
|
raise NotImplementedError('update active action is not implemented')
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def query(self,
|
def query(self,
|
||||||
@ -84,7 +88,7 @@ class ActiveActionsConnection(object):
|
|||||||
|
|
||||||
:rtype: list of vitrage.storage.sqlalchemy.models.ActiveAction
|
:rtype: list of vitrage.storage.sqlalchemy.models.ActiveAction
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('query active actions not implemented')
|
raise NotImplementedError('query active actions is not implemented')
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def delete(self,
|
def delete(self,
|
||||||
@ -97,7 +101,40 @@ class ActiveActionsConnection(object):
|
|||||||
trigger=None,
|
trigger=None,
|
||||||
):
|
):
|
||||||
"""Delete all active actions that match the filters."""
|
"""Delete all active actions that match the filters."""
|
||||||
raise NotImplementedError('delete active actions not implemented')
|
raise NotImplementedError('delete active actions is not implemented')
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class WebhooksConnection(object):
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create(self, webhook):
|
||||||
|
"""Create a new webhook.
|
||||||
|
|
||||||
|
:type webhook:
|
||||||
|
vitrage.storage.sqlalchemy.models.Webhook
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('create webhook is not implemented')
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def query(self,
|
||||||
|
id=None,
|
||||||
|
project_id=None,
|
||||||
|
is_admin_webhook=None,
|
||||||
|
url=None,
|
||||||
|
headers=None,
|
||||||
|
regex_filter=None,
|
||||||
|
):
|
||||||
|
"""Yields a lists of webhooks that match filters.
|
||||||
|
|
||||||
|
:rtype: list of vitrage.storage.sqlalchemy.models.Webhook
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('query webhook is not implemented')
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def delete(self, id=None):
|
||||||
|
"""Delete all webhooks that match the filters."""
|
||||||
|
raise NotImplementedError('delete webhook is not implemented')
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
@ -145,14 +182,14 @@ class EventsConnection(object):
|
|||||||
|
|
||||||
:type event: vitrage.storage.sqlalchemy.models.Event
|
:type event: vitrage.storage.sqlalchemy.models.Event
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('create event not implemented')
|
raise NotImplementedError('create event is not implemented')
|
||||||
|
|
||||||
def update(self, event):
|
def update(self, event):
|
||||||
"""Update an existing event.
|
"""Update an existing event.
|
||||||
|
|
||||||
:type event: vitrage.storage.sqlalchemy.models.Event
|
:type event: vitrage.storage.sqlalchemy.models.Event
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('update event not implemented')
|
raise NotImplementedError('update event is not implemented')
|
||||||
|
|
||||||
def query(self,
|
def query(self,
|
||||||
event_id=None,
|
event_id=None,
|
||||||
@ -164,7 +201,7 @@ class EventsConnection(object):
|
|||||||
|
|
||||||
:rtype: list of vitrage.storage.sqlalchemy.models.Event
|
:rtype: list of vitrage.storage.sqlalchemy.models.Event
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('query events not implemented')
|
raise NotImplementedError('query events is not implemented')
|
||||||
|
|
||||||
def delete(self,
|
def delete(self,
|
||||||
event_id=None,
|
event_id=None,
|
||||||
@ -172,7 +209,7 @@ class EventsConnection(object):
|
|||||||
gt_collector_timestamp=None,
|
gt_collector_timestamp=None,
|
||||||
lt_collector_timestamp=None):
|
lt_collector_timestamp=None):
|
||||||
"""Delete all events that match the filters."""
|
"""Delete all events that match the filters."""
|
||||||
raise NotImplementedError('delete events not implemented')
|
raise NotImplementedError('delete events is not implemented')
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
@ -44,6 +44,12 @@ class Connection(base.Connection):
|
|||||||
self._events = EventsConnection(self._engine_facade)
|
self._events = EventsConnection(self._engine_facade)
|
||||||
self._templates = TemplatesConnection(self._engine_facade)
|
self._templates = TemplatesConnection(self._engine_facade)
|
||||||
self._graph_snapshots = GraphSnapshotsConnection(self._engine_facade)
|
self._graph_snapshots = GraphSnapshotsConnection(self._engine_facade)
|
||||||
|
self._webhooks = WebhooksConnection(
|
||||||
|
self._engine_facade)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def webhooks(self):
|
||||||
|
return self._webhooks
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_actions(self):
|
def active_actions(self):
|
||||||
@ -191,6 +197,38 @@ class ActiveActionsConnection(base.ActiveActionsConnection, BaseTableConn):
|
|||||||
return query.delete()
|
return query.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class WebhooksConnection(base.WebhooksConnection,
|
||||||
|
BaseTableConn):
|
||||||
|
def __init__(self, engine_facade):
|
||||||
|
super(WebhooksConnection, self).__init__(engine_facade)
|
||||||
|
|
||||||
|
def create(self, webhook):
|
||||||
|
session = self._engine_facade.get_session()
|
||||||
|
with session.begin():
|
||||||
|
session.add(webhook)
|
||||||
|
|
||||||
|
def query(self,
|
||||||
|
id=None,
|
||||||
|
project_id=None,
|
||||||
|
is_admin_webhook=None,
|
||||||
|
url=None,
|
||||||
|
headers=None,
|
||||||
|
regex_filter=None):
|
||||||
|
query = self.query_filter(
|
||||||
|
models.Webhooks,
|
||||||
|
id=id,
|
||||||
|
project_id=project_id,
|
||||||
|
is_admin_webhook=is_admin_webhook,
|
||||||
|
url=url,
|
||||||
|
headers=headers,
|
||||||
|
regex_filter=regex_filter)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
def delete(self, id=None):
|
||||||
|
query = self.query_filter(models.Webhooks, id=id)
|
||||||
|
return query.delete()
|
||||||
|
|
||||||
|
|
||||||
class EventsConnection(base.EventsConnection, BaseTableConn):
|
class EventsConnection(base.EventsConnection, BaseTableConn):
|
||||||
def __init__(self, engine_facade):
|
def __init__(self, engine_facade):
|
||||||
super(EventsConnection, self).__init__(engine_facade)
|
super(EventsConnection, self).__init__(engine_facade)
|
||||||
|
@ -16,13 +16,13 @@ import json
|
|||||||
from oslo_db.sqlalchemy import models
|
from oslo_db.sqlalchemy import models
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, INTEGER, String, \
|
from sqlalchemy import Column, DateTime, INTEGER, String, \
|
||||||
SmallInteger, BigInteger, Index
|
SmallInteger, BigInteger, Index, Boolean, UniqueConstraint
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
import sqlalchemy.types as types
|
import sqlalchemy.types as types
|
||||||
|
|
||||||
|
|
||||||
class VitrageBase(models.ModelBase):
|
class VitrageBase(models.TimestampMixin, models.ModelBase):
|
||||||
"""Base class for Vitrage Models."""
|
"""Base class for Vitrage Models."""
|
||||||
__table_args__ = {'mysql_charset': "utf8",
|
__table_args__ = {'mysql_charset': "utf8",
|
||||||
'mysql_engine': "InnoDB"}
|
'mysql_engine': "InnoDB"}
|
||||||
@ -161,3 +161,37 @@ class Template(Base, models.TimestampMixin):
|
|||||||
self.status_details,
|
self.status_details,
|
||||||
self.file_content,
|
self.file_content,
|
||||||
self.template_type,)
|
self.template_type,)
|
||||||
|
|
||||||
|
|
||||||
|
class Webhooks(Base):
|
||||||
|
__tablename__ = 'webhooks'
|
||||||
|
|
||||||
|
id = Column(String(128), primary_key=True)
|
||||||
|
project_id = Column(String(128), nullable=False)
|
||||||
|
is_admin_webhook = Column(Boolean, nullable=False)
|
||||||
|
url = Column(String(256), nullable=False)
|
||||||
|
headers = Column(String(1024))
|
||||||
|
regex_filter = Column(String(512))
|
||||||
|
constraint = UniqueConstraint('url', 'regex_filter')
|
||||||
|
|
||||||
|
__table_args__ = (UniqueConstraint('url', 'regex_filter'),)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return \
|
||||||
|
"<Webhook(" \
|
||||||
|
"id='%s', " \
|
||||||
|
"created_at='%s', " \
|
||||||
|
"project_id='%s', " \
|
||||||
|
"is_admin_webhook='%s', " \
|
||||||
|
"url='%s', " \
|
||||||
|
"headers='%s', " \
|
||||||
|
"regex_filter='%s')> " %\
|
||||||
|
(
|
||||||
|
self.id,
|
||||||
|
self.created_at,
|
||||||
|
self.project_id,
|
||||||
|
self.is_admin_webhook,
|
||||||
|
self.url,
|
||||||
|
self.headers,
|
||||||
|
self.regex_filter
|
||||||
|
)
|
||||||
|
15
vitrage_tempest_tests/tests/api/webhook/__init__.py
Normal file
15
vitrage_tempest_tests/tests/api/webhook/__init__.py
Normal file
@ -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'
|
149
vitrage_tempest_tests/tests/api/webhook/test_webhook.py
Normal file
149
vitrage_tempest_tests/tests/api/webhook/test_webhook.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from vitrage_tempest_tests.tests.base import BaseVitrageTempest
|
||||||
|
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
||||||
|
from vitrageclient.exceptions import ClientException
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
URL = 'url'
|
||||||
|
REGEX_FILTER = 'regex_filter'
|
||||||
|
HEADERS = 'headers'
|
||||||
|
HEADERS_PROPS = '{"content": "application/json"}'
|
||||||
|
REGEX_PROPS = '{"name": "e2e.*"}'
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebhook(BaseVitrageTempest):
|
||||||
|
"""Webhook test class for Vitrage API tests."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestWebhook, cls).setUpClass()
|
||||||
|
cls.pre_test_webhook_count = \
|
||||||
|
len(TempestClients.vitrage().webhook.list())
|
||||||
|
|
||||||
|
def test_add_webhook(self):
|
||||||
|
|
||||||
|
webhooks = TempestClients.vitrage().webhook.list()
|
||||||
|
self.assertEqual(self.pre_test_webhook_count,
|
||||||
|
len(webhooks),
|
||||||
|
'Amount of webhooks should be the same as '
|
||||||
|
'before the test')
|
||||||
|
|
||||||
|
created_webhook = TempestClients.vitrage().webhook.add(
|
||||||
|
url="https://www.test.com",
|
||||||
|
regex_filter=REGEX_PROPS,
|
||||||
|
headers=HEADERS_PROPS
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(created_webhook.get('ERROR'), 'webhook not '
|
||||||
|
'created')
|
||||||
|
self.assertEqual(created_webhook[HEADERS],
|
||||||
|
HEADERS_PROPS,
|
||||||
|
'headers not created correctly')
|
||||||
|
self.assertEqual(created_webhook[REGEX_FILTER],
|
||||||
|
REGEX_PROPS,
|
||||||
|
'regex not created correctly')
|
||||||
|
self.assertEqual(created_webhook[URL],
|
||||||
|
"https://www.test.com",
|
||||||
|
'URL not created correctly')
|
||||||
|
|
||||||
|
webhooks = TempestClients.vitrage().webhook.list()
|
||||||
|
|
||||||
|
self.assertEqual(self.pre_test_webhook_count + 1, len(webhooks))
|
||||||
|
TempestClients.vitrage().webhook.delete(
|
||||||
|
created_webhook['id'])
|
||||||
|
|
||||||
|
def test_delete_webhook(self):
|
||||||
|
webhooks = TempestClients.vitrage().webhook.list()
|
||||||
|
self.assertEqual(self.pre_test_webhook_count,
|
||||||
|
len(webhooks),
|
||||||
|
'Amount of webhooks should be the same as '
|
||||||
|
'before the test')
|
||||||
|
|
||||||
|
created_webhook = TempestClients.vitrage().webhook.add(
|
||||||
|
url="https://www.test.com",
|
||||||
|
regex_filter=REGEX_PROPS,
|
||||||
|
headers=HEADERS_PROPS
|
||||||
|
)
|
||||||
|
|
||||||
|
created_webhook = TempestClients.vitrage().webhook.delete(
|
||||||
|
id=created_webhook['id'])
|
||||||
|
self.assertIsNotNone(created_webhook.get('SUCCESS'),
|
||||||
|
'failed to delete')
|
||||||
|
self.assertEqual(self.pre_test_webhook_count, len(webhooks),
|
||||||
|
'No webhooks should exist after deletion')
|
||||||
|
|
||||||
|
def test_delete_non_existing_webhook(self):
|
||||||
|
self.assertRaises(ClientException,
|
||||||
|
TempestClients.vitrage().webhook.delete,
|
||||||
|
('non existant'))
|
||||||
|
|
||||||
|
def test_list_webhook(self):
|
||||||
|
|
||||||
|
webhooks = TempestClients.vitrage().webhook.list()
|
||||||
|
self.assertEqual(self.pre_test_webhook_count,
|
||||||
|
len(webhooks),
|
||||||
|
'Amount of webhooks should be the same as '
|
||||||
|
'before the test')
|
||||||
|
|
||||||
|
created_webhook = TempestClients.vitrage().webhook.add(
|
||||||
|
url="https://www.test.com",
|
||||||
|
regex_filter=REGEX_PROPS,
|
||||||
|
headers=HEADERS_PROPS
|
||||||
|
)
|
||||||
|
|
||||||
|
webhooks = TempestClients.vitrage().webhook.list()
|
||||||
|
self.assertEqual(self.pre_test_webhook_count + 1, len(webhooks))
|
||||||
|
self.assertEqual(created_webhook[HEADERS], webhooks[0][HEADERS])
|
||||||
|
self.assertEqual(created_webhook['id'], webhooks[0]['id'])
|
||||||
|
self.assertEqual(created_webhook[REGEX_FILTER],
|
||||||
|
webhooks[0][REGEX_FILTER])
|
||||||
|
|
||||||
|
TempestClients.vitrage().webhook.delete(
|
||||||
|
created_webhook['id'])
|
||||||
|
|
||||||
|
def test_show_webhook(self):
|
||||||
|
webhooks = TempestClients.vitrage().webhook.list()
|
||||||
|
self.assertEqual(self.pre_test_webhook_count,
|
||||||
|
len(webhooks),
|
||||||
|
'Amount of webhooks should be the same as '
|
||||||
|
'before the test')
|
||||||
|
|
||||||
|
created_webhook = TempestClients.vitrage().webhook.add(
|
||||||
|
url="https://www.test.com",
|
||||||
|
regex_filter=REGEX_PROPS,
|
||||||
|
headers=HEADERS_PROPS
|
||||||
|
)
|
||||||
|
|
||||||
|
show_webhook = TempestClients.vitrage().webhook.show(
|
||||||
|
created_webhook['id']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(show_webhook, 'webhook not listed')
|
||||||
|
self.assertEqual(created_webhook[HEADERS],
|
||||||
|
show_webhook[HEADERS],
|
||||||
|
'headers mismatch')
|
||||||
|
self.assertEqual(created_webhook[REGEX_FILTER],
|
||||||
|
show_webhook[REGEX_FILTER],
|
||||||
|
'regex mismatch')
|
||||||
|
self.assertEqual(created_webhook[URL],
|
||||||
|
show_webhook[URL],
|
||||||
|
'URL mismatch')
|
||||||
|
|
||||||
|
TempestClients.vitrage().webhook.delete(
|
||||||
|
created_webhook['id'])
|
268
vitrage_tempest_tests/tests/e2e/test_e2e_webhook.py
Normal file
268
vitrage_tempest_tests/tests/e2e/test_e2e_webhook.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import requests
|
||||||
|
from six.moves import BaseHTTPServer
|
||||||
|
import socket
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients
|
||||||
|
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'
|
||||||
|
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"}'
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebhook(TestActionsBase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestWebhook, cls).setUpClass()
|
||||||
|
# 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
|
||||||
|
|
||||||
|
@utils.tempest_logger
|
||||||
|
def test_webhook_basic_event(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Add webhook with filter matching alarm
|
||||||
|
TempestClients.vitrage().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.assertEqual(1, len(self.mock_server.requests),
|
||||||
|
'Wrong number of notifications for raise alarm')
|
||||||
|
|
||||||
|
# Undo
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
|
||||||
|
# Check event undo received
|
||||||
|
self.assertEqual(2, len(self.mock_server.requests),
|
||||||
|
'Wrong number of notifications for clear alarm')
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._delete_webhooks()
|
||||||
|
self._trigger_undo_action(TRIGGER_ALARM_1)
|
||||||
|
self.mock_server.reset_requests_list()
|
||||||
|
|
||||||
|
def test_webhook_with_no_filter(self):
|
||||||
|
"""Test to see that a webhook with no filter receives all
|
||||||
|
|
||||||
|
notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Add webhook
|
||||||
|
TempestClients.vitrage().webhook.add(
|
||||||
|
url=self.URL_PROPS,
|
||||||
|
regex_filter=NAME_FILTER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Raise alarm
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_1)
|
||||||
|
|
||||||
|
# Check event received
|
||||||
|
self.assertEqual(1, len(self.mock_server.requests),
|
||||||
|
'Wrong number of notifications for raise alarm')
|
||||||
|
|
||||||
|
# Raise another alarm
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_2)
|
||||||
|
|
||||||
|
# Check second event received
|
||||||
|
self.assertEqual(2, len(self.mock_server.requests),
|
||||||
|
'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()
|
||||||
|
|
||||||
|
def test_webhook_with_no_match(self):
|
||||||
|
"""Test to check that filters with no match do not send event """
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Add webhook
|
||||||
|
TempestClients.vitrage().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.assertEqual(0, len(self.mock_server.requests),
|
||||||
|
'event should not have passed filter')
|
||||||
|
|
||||||
|
# Raise another alarm
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_2)
|
||||||
|
|
||||||
|
# Check second event not received
|
||||||
|
self.assertEqual(0, len(self.mock_server.requests),
|
||||||
|
'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()
|
||||||
|
|
||||||
|
def test_multiple_webhooks(self):
|
||||||
|
"""Test to check filter by type and by ID (with 2 different
|
||||||
|
|
||||||
|
webhooks)
|
||||||
|
"""
|
||||||
|
|
||||||
|
host_id = self.orig_host[VProps.VITRAGE_ID]
|
||||||
|
ID_FILTER = '{"%s": "%s"}' % (VProps.VITRAGE_RESOURCE_ID, host_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Add webhook
|
||||||
|
TempestClients.vitrage().webhook.add(
|
||||||
|
url=self.URL_PROPS,
|
||||||
|
regex_filter=TYPE_FILTER,
|
||||||
|
)
|
||||||
|
|
||||||
|
TempestClients.vitrage().webhook.add(
|
||||||
|
url=self.URL_PROPS,
|
||||||
|
regex_filter=ID_FILTER,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Raise alarm
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_1)
|
||||||
|
|
||||||
|
# Check event received
|
||||||
|
self.assertEqual(2, len(self.mock_server.requests),
|
||||||
|
'event not posted to all webhooks')
|
||||||
|
|
||||||
|
# Raise another alarm
|
||||||
|
self._trigger_do_action(TRIGGER_ALARM_2)
|
||||||
|
|
||||||
|
# Check second event received
|
||||||
|
self.assertEqual(4, len(self.mock_server.requests),
|
||||||
|
'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()
|
||||||
|
|
||||||
|
def _delete_webhooks(self):
|
||||||
|
webhooks = TempestClients.vitrage().webhook.list()
|
||||||
|
for webhook in webhooks:
|
||||||
|
TempestClients.vitrage().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', str(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
|
||||||
|
self.send_response(requests.codes.ok)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
@ -0,0 +1,29 @@
|
|||||||
|
metadata:
|
||||||
|
name: e2e_webhook
|
||||||
|
definitions:
|
||||||
|
entities:
|
||||||
|
- entity:
|
||||||
|
category: ALARM
|
||||||
|
name: e2e.test_webhook.alarm_with_deduced
|
||||||
|
template_id: host_alarm
|
||||||
|
- entity:
|
||||||
|
category: RESOURCE
|
||||||
|
type: nova.host
|
||||||
|
template_id: host
|
||||||
|
relationships:
|
||||||
|
- relationship:
|
||||||
|
source: host_alarm
|
||||||
|
target: host
|
||||||
|
relationship_type: on
|
||||||
|
template_id : alarm_on_host
|
||||||
|
scenarios:
|
||||||
|
- scenario:
|
||||||
|
condition: alarm_on_host
|
||||||
|
actions:
|
||||||
|
- action:
|
||||||
|
action_type: raise_alarm
|
||||||
|
action_target:
|
||||||
|
target: host
|
||||||
|
properties:
|
||||||
|
alarm_name: e2e.test_webhook.deduced
|
||||||
|
severity: WARNING
|
Loading…
x
Reference in New Issue
Block a user