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:
licanwei
2019-12-13 11:01:35 +08:00
parent 0c02b08a6a
commit 775be27719
9 changed files with 169 additions and 3 deletions

View File

@@ -38,6 +38,7 @@ app = {
'enable_acl': True,
'acl_public_routes': [
'/',
'/v1/webhooks/.*',
],
}

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View 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)

View File

@@ -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")

View File

@@ -75,6 +75,7 @@ class State(object):
class AuditType(enum.Enum):
ONESHOT = 'ONESHOT'
CONTINUOUS = 'CONTINUOUS'
EVENT = 'EVENT'
@base.WatcherObjectRegistry.register

View File

@@ -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',

View 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'])