Merge "Add webhook api"
This commit is contained in:
@@ -38,6 +38,7 @@ app = {
|
|||||||
'enable_acl': True,
|
'enable_acl': True,
|
||||||
'acl_public_routes': [
|
'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 strategy
|
||||||
from watcher.api.controllers.v1 import utils
|
from watcher.api.controllers.v1 import utils
|
||||||
from watcher.api.controllers.v1 import versions
|
from watcher.api.controllers.v1 import versions
|
||||||
|
from watcher.api.controllers.v1 import webhooks
|
||||||
|
|
||||||
|
|
||||||
def min_version():
|
def min_version():
|
||||||
@@ -131,6 +132,9 @@ class V1(APIBase):
|
|||||||
services = [link.Link]
|
services = [link.Link]
|
||||||
"""Links to the services resource"""
|
"""Links to the services resource"""
|
||||||
|
|
||||||
|
webhooks = [link.Link]
|
||||||
|
"""Links to the webhooks resource"""
|
||||||
|
|
||||||
links = [link.Link]
|
links = [link.Link]
|
||||||
"""Links that point to a specific URL for this version and documentation"""
|
"""Links that point to a specific URL for this version and documentation"""
|
||||||
|
|
||||||
@@ -202,6 +206,14 @@ class V1(APIBase):
|
|||||||
'services', '',
|
'services', '',
|
||||||
bookmark=True)
|
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
|
return v1
|
||||||
|
|
||||||
|
|
||||||
@@ -217,6 +229,7 @@ class Controller(rest.RestController):
|
|||||||
services = service.ServicesController()
|
services = service.ServicesController()
|
||||||
strategies = strategy.StrategiesController()
|
strategies = strategy.StrategiesController()
|
||||||
data_model = data_model.DataModelController()
|
data_model = data_model.DataModelController()
|
||||||
|
webhooks = webhooks.WebhookController()
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(V1)
|
@wsme_pecan.wsexpose(V1)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
|||||||
@@ -185,3 +185,12 @@ def allow_list_datamodel():
|
|||||||
"""
|
"""
|
||||||
return pecan.request.version.minor >= (
|
return pecan.request.version.minor >= (
|
||||||
versions.VERSIONS.MINOR_3_DATAMODEL.value)
|
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_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_2_FORCE = 2 # v1.2: Add force field to audit
|
||||||
MINOR_3_DATAMODEL = 3 # v1.3: Add list datamodel API
|
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
|
# This is the version 1 API
|
||||||
BASE_VERSION = 1
|
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")
|
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):
|
class AuditParameterNotAllowed(Invalid):
|
||||||
msg_fmt = _("Audit parameter %(parameter)s are not allowed")
|
msg_fmt = _("Audit parameter %(parameter)s are not allowed")
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class State(object):
|
|||||||
class AuditType(enum.Enum):
|
class AuditType(enum.Enum):
|
||||||
ONESHOT = 'ONESHOT'
|
ONESHOT = 'ONESHOT'
|
||||||
CONTINUOUS = 'CONTINUOUS'
|
CONTINUOUS = 'CONTINUOUS'
|
||||||
|
EVENT = 'EVENT'
|
||||||
|
|
||||||
|
|
||||||
@base.WatcherObjectRegistry.register
|
@base.WatcherObjectRegistry.register
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class TestV1Root(base.FunctionalTest):
|
|||||||
|
|
||||||
def test_get_v1_root_all(self):
|
def test_get_v1_root_all(self):
|
||||||
data = self.get_json(
|
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'])
|
self.assertEqual('v1', data['id'])
|
||||||
# Check fields are not empty
|
# Check fields are not empty
|
||||||
for f in data.keys():
|
for f in data.keys():
|
||||||
@@ -39,7 +39,7 @@ class TestV1Root(base.FunctionalTest):
|
|||||||
actual_resources = tuple(set(data.keys()) - set(not_resources))
|
actual_resources = tuple(set(data.keys()) - set(not_resources))
|
||||||
expected_resources = ('audit_templates', 'audits', 'actions',
|
expected_resources = ('audit_templates', 'audits', 'actions',
|
||||||
'action_plans', 'data_model', 'scoring_engines',
|
'action_plans', 'data_model', 'scoring_engines',
|
||||||
'services')
|
'services', 'webhooks')
|
||||||
self.assertEqual(sorted(expected_resources), sorted(actual_resources))
|
self.assertEqual(sorted(expected_resources), sorted(actual_resources))
|
||||||
|
|
||||||
self.assertIn({'type': 'application/vnd.openstack.watcher.v1+json',
|
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