Add webhook api
Add a new webhook api and its microversion is 1.4 Partially Implements: blueprint event-driven-optimization-based Change-Id: I50f7c824e52f3c5fc775d5064898ed422e375a99
This commit is contained in:
@@ -38,6 +38,7 @@ app = {
|
||||
'enable_acl': True,
|
||||
'acl_public_routes': [
|
||||
'/',
|
||||
'/v1/webhooks/.*',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ from watcher.api.controllers.v1 import service
|
||||
from watcher.api.controllers.v1 import strategy
|
||||
from watcher.api.controllers.v1 import utils
|
||||
from watcher.api.controllers.v1 import versions
|
||||
from watcher.api.controllers.v1 import webhooks
|
||||
|
||||
|
||||
def min_version():
|
||||
@@ -131,6 +132,9 @@ class V1(APIBase):
|
||||
services = [link.Link]
|
||||
"""Links to the services resource"""
|
||||
|
||||
webhooks = [link.Link]
|
||||
"""Links to the webhooks resource"""
|
||||
|
||||
links = [link.Link]
|
||||
"""Links that point to a specific URL for this version and documentation"""
|
||||
|
||||
@@ -202,6 +206,14 @@ class V1(APIBase):
|
||||
'services', '',
|
||||
bookmark=True)
|
||||
]
|
||||
if utils.allow_webhook_api():
|
||||
v1.webhooks = [link.Link.make_link(
|
||||
'self', base_url, 'webhooks', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
base_url,
|
||||
'webhooks', '',
|
||||
bookmark=True)
|
||||
]
|
||||
return v1
|
||||
|
||||
|
||||
@@ -217,6 +229,7 @@ class Controller(rest.RestController):
|
||||
services = service.ServicesController()
|
||||
strategies = strategy.StrategiesController()
|
||||
data_model = data_model.DataModelController()
|
||||
webhooks = webhooks.WebhookController()
|
||||
|
||||
@wsme_pecan.wsexpose(V1)
|
||||
def get(self):
|
||||
|
||||
@@ -185,3 +185,12 @@ def allow_list_datamodel():
|
||||
"""
|
||||
return pecan.request.version.minor >= (
|
||||
versions.VERSIONS.MINOR_3_DATAMODEL.value)
|
||||
|
||||
|
||||
def allow_webhook_api():
|
||||
"""Check if we should support webhook API.
|
||||
|
||||
Version 1.4 of the API added support to trigger webhook.
|
||||
"""
|
||||
return pecan.request.version.minor >= (
|
||||
versions.VERSIONS.MINOR_4_WEBHOOK_API.value)
|
||||
|
||||
@@ -22,7 +22,8 @@ class VERSIONS(enum.Enum):
|
||||
MINOR_1_START_END_TIMING = 1 # v1.1: Add start/end timei for audit
|
||||
MINOR_2_FORCE = 2 # v1.2: Add force field to audit
|
||||
MINOR_3_DATAMODEL = 3 # v1.3: Add list datamodel API
|
||||
MINOR_MAX_VERSION = 3
|
||||
MINOR_4_WEBHOOK_API = 4 # v1.4: Add webhook trigger API
|
||||
MINOR_MAX_VERSION = 4
|
||||
|
||||
# This is the version 1 API
|
||||
BASE_VERSION = 1
|
||||
|
||||
62
watcher/api/controllers/v1/webhooks.py
Normal file
62
watcher/api/controllers/v1/webhooks.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Webhook endpoint for Watcher v1 REST API.
|
||||
"""
|
||||
|
||||
from oslo_log import log
|
||||
import pecan
|
||||
from pecan import rest
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from watcher.api.controllers.v1 import types
|
||||
from watcher.api.controllers.v1 import utils
|
||||
from watcher.common import exception
|
||||
from watcher.decision_engine import rpcapi
|
||||
from watcher import objects
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class WebhookController(rest.RestController):
|
||||
"""REST controller for webhooks resource."""
|
||||
def __init__(self):
|
||||
super(WebhookController, self).__init__()
|
||||
self.dc_client = rpcapi.DecisionEngineAPI()
|
||||
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, body=types.jsontype,
|
||||
status_code=202)
|
||||
def post(self, audit_ident, body):
|
||||
"""Trigger the given audit.
|
||||
|
||||
:param audit_ident: UUID or name of an audit.
|
||||
"""
|
||||
|
||||
LOG.debug("Webhook trigger Audit: %s.", audit_ident)
|
||||
|
||||
context = pecan.request.context
|
||||
audit = utils.get_resource('Audit', audit_ident)
|
||||
if audit is None:
|
||||
raise exception.AuditNotFound(audit=audit_ident)
|
||||
if audit.audit_type != objects.audit.AuditType.EVENT.value:
|
||||
raise exception.AuditTypeNotAllowed(audit_type=audit.audit_type)
|
||||
allowed_state = (
|
||||
objects.audit.State.PENDING,
|
||||
objects.audit.State.SUCCEEDED,
|
||||
)
|
||||
if audit.state not in allowed_state:
|
||||
raise exception.AuditStateNotAllowed(state=audit.state)
|
||||
|
||||
# trigger decision-engine to run the audit
|
||||
self.dc_client.trigger_audit(context, audit.uuid)
|
||||
@@ -243,6 +243,14 @@ class AuditTypeNotFound(Invalid):
|
||||
msg_fmt = _("Audit type %(audit_type)s could not be found")
|
||||
|
||||
|
||||
class AuditTypeNotAllowed(Invalid):
|
||||
msg_fmt = _("Audit type %(audit_type)s is disallowed.")
|
||||
|
||||
|
||||
class AuditStateNotAllowed(Invalid):
|
||||
msg_fmt = _("Audit state %(state)s is disallowed.")
|
||||
|
||||
|
||||
class AuditParameterNotAllowed(Invalid):
|
||||
msg_fmt = _("Audit parameter %(parameter)s are not allowed")
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ class State(object):
|
||||
class AuditType(enum.Enum):
|
||||
ONESHOT = 'ONESHOT'
|
||||
CONTINUOUS = 'CONTINUOUS'
|
||||
EVENT = 'EVENT'
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register
|
||||
|
||||
@@ -29,7 +29,7 @@ class TestV1Root(base.FunctionalTest):
|
||||
|
||||
def test_get_v1_root_all(self):
|
||||
data = self.get_json(
|
||||
'/', headers={'OpenStack-API-Version': 'infra-optim 1.3'})
|
||||
'/', headers={'OpenStack-API-Version': 'infra-optim 1.4'})
|
||||
self.assertEqual('v1', data['id'])
|
||||
# Check fields are not empty
|
||||
for f in data.keys():
|
||||
@@ -39,7 +39,7 @@ class TestV1Root(base.FunctionalTest):
|
||||
actual_resources = tuple(set(data.keys()) - set(not_resources))
|
||||
expected_resources = ('audit_templates', 'audits', 'actions',
|
||||
'action_plans', 'data_model', 'scoring_engines',
|
||||
'services')
|
||||
'services', 'webhooks')
|
||||
self.assertEqual(sorted(expected_resources), sorted(actual_resources))
|
||||
|
||||
self.assertIn({'type': 'application/vnd.openstack.watcher.v1+json',
|
||||
|
||||
71
watcher/tests/api/v1/test_webhooks.py
Normal file
71
watcher/tests/api/v1/test_webhooks.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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 mock
|
||||
|
||||
from watcher.decision_engine import rpcapi as deapi
|
||||
from watcher import objects
|
||||
from watcher.tests.api import base as api_base
|
||||
from watcher.tests.objects import utils as obj_utils
|
||||
|
||||
|
||||
class TestPost(api_base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPost, self).setUp()
|
||||
obj_utils.create_test_goal(self.context)
|
||||
obj_utils.create_test_strategy(self.context)
|
||||
obj_utils.create_test_audit_template(self.context)
|
||||
|
||||
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
|
||||
def test_trigger_audit(self, mock_trigger_audit):
|
||||
audit = obj_utils.create_test_audit(
|
||||
self.context,
|
||||
audit_type=objects.audit.AuditType.EVENT.value)
|
||||
response = self.post_json(
|
||||
'/webhooks/%s' % audit['uuid'], {},
|
||||
headers={'OpenStack-API-Version': 'infra-optim 1.4'})
|
||||
self.assertEqual(202, response.status_int)
|
||||
mock_trigger_audit.assert_called_once_with(
|
||||
mock.ANY, audit['uuid'])
|
||||
|
||||
def test_trigger_audit_with_no_audit(self):
|
||||
response = self.post_json(
|
||||
'/webhooks/no-audit', {},
|
||||
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
|
||||
expect_errors=True)
|
||||
self.assertEqual(404, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_trigger_audit_with_not_allowed_audittype(self):
|
||||
audit = obj_utils.create_test_audit(self.context)
|
||||
response = self.post_json(
|
||||
'/webhooks/%s' % audit['uuid'], {},
|
||||
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
|
||||
expect_errors=True)
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_trigger_audit_with_not_allowed_audit_state(self):
|
||||
audit = obj_utils.create_test_audit(
|
||||
self.context,
|
||||
audit_type=objects.audit.AuditType.EVENT.value,
|
||||
state=objects.audit.State.FAILED)
|
||||
response = self.post_json(
|
||||
'/webhooks/%s' % audit['uuid'], {},
|
||||
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
|
||||
expect_errors=True)
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
Reference in New Issue
Block a user