Implement RestAPIs of trigger.

Includes create/delete/get/list APIs.

Change-Id: I03deb689e48d30b44eadc6badf41f1b6d6f6c935
Closes-Bug: #1551569
This commit is contained in:
zengchen 2016-03-01 14:33:58 +08:00
parent 4ddbe722f6
commit d1d69da052
9 changed files with 538 additions and 12 deletions

View File

@ -24,5 +24,11 @@
"provider:checkpoint_get": "rule:admin_or_owner",
"provider:checkpoint_get_all": "rule:admin_or_owner",
"provider:checkpoint_create": "rule:admin_or_owner",
"provider:checkpoint_delete": "rule:admin_or_owner"
"provider:checkpoint_delete": "rule:admin_or_owner",
"trigger:create": "",
"trigger:delete": "rule:admin_or_owner",
"trigger:update": "rule:admin_or_owner",
"trigger:get": "rule:admin_or_owner",
"trigger:list": ""
}

View File

@ -74,7 +74,7 @@ def _get_limit_param(params, max_limit=None):
except ValueError:
msg = _('limit param must be an integer')
raise webob.exc.HTTPBadRequest(explanation=msg)
if limit < 0:
if limit <= 0:
msg = _('limit param must be positive')
raise webob.exc.HTTPBadRequest(explanation=msg)
limit = min(limit, max_limit)

View File

@ -16,6 +16,7 @@ from smaug.api.v1 import protectables
from smaug.api.v1 import providers
from smaug.api.v1 import restores
from smaug.api.v1 import scheduled_operations
from smaug.api.v1 import triggers
from smaug.wsgi import common as wsgi_common
@ -29,7 +30,9 @@ class APIRouter(wsgi_common.Router):
restores_resources = restores.create_resource()
protectables_resources = protectables.create_resource()
providers_resources = providers.create_resource()
trigger_resources = triggers.create_resource()
scheduled_operation_resources = scheduled_operations.create_resource()
mapper.resource("plan", "plans",
controller=plans_resources,
collection={},
@ -74,6 +77,10 @@ class APIRouter(wsgi_common.Router):
controller=providers_resources,
action='checkpoints_delete',
conditions={"method": ['DELETE']})
mapper.resource("trigger", "triggers",
controller=trigger_resources,
collection={},
member={'action': 'POST'})
mapper.resource("scheduled_operation", "scheduled_operations",
controller=scheduled_operation_resources,
collection={'detail': 'GET'},

242
smaug/api/v1/triggers.py Normal file
View File

@ -0,0 +1,242 @@
# 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.
"""The triggers api."""
from oslo_log import log as logging
from oslo_utils import uuidutils
import uuid
from webob import exc
from smaug.api import common
from smaug.api.openstack import wsgi
from smaug import exception
from smaug.i18n import _
from smaug import objects
from smaug.operationengine import api as operationengine_api
from smaug import policy
from smaug import utils
LOG = logging.getLogger(__name__)
def check_policy(context, action, target_obj=None):
_action = 'trigger:%s' % action
policy.enforce(context, _action, target_obj)
class TriggerViewBuilder(common.ViewBuilder):
"""Model a trigger API response as a python dictionary."""
_collection_name = "triggers"
def detail(self, request, trigger):
"""Detailed view of a single trigger."""
trigger_ref = {
'trigger_info': {
'id': trigger.get('id'),
'name': trigger.get('name'),
'type': trigger.get('type'),
'properties': trigger.get('properties'),
}
}
return trigger_ref
def detail_list(self, request, triggers):
"""Detailed view of a list of triggers."""
return self._list_view(self.detail, request, triggers)
def _list_view(self, func, request, triggers):
triggers_list = [func(request, item)['trigger_info']
for item in triggers]
triggers_links = self._get_collection_links(request,
triggers,
self._collection_name,
)
ret = {'triggers': triggers_list}
if triggers_links:
ret['triggers_links'] = triggers_links
return ret
class TriggersController(wsgi.Controller):
"""The Triggers API controller for the OpenStack API."""
_view_builder_class = TriggerViewBuilder
def __init__(self):
self.operationengine_api = operationengine_api.API()
super(TriggersController, self).__init__()
def create(self, req, body):
"""Creates a new trigger."""
LOG.debug('Create trigger start')
if not self.is_valid_body(body, 'trigger_info'):
raise exc.HTTPUnprocessableEntity()
LOG.debug('Create a trigger, request body: %s', body)
context = req.environ['smaug.context']
check_policy(context, 'create')
trigger_info = body['trigger_info']
trigger_name = trigger_info.get("name", None)
trigger_type = trigger_info.get("type", None)
trigger_property = trigger_info.get("properties", None)
if not trigger_name or not trigger_type or not trigger_property:
msg = _("Trigger name or type or property is not provided.")
raise exc.HTTPBadRequest(explanation=msg)
self.validate_name_and_description(trigger_info)
trigger_definition = {
'id': str(uuid.uuid4()),
'name': trigger_name,
'project_id': context.project_id,
'type': trigger_type,
'properties': trigger_property,
}
try:
trigger = objects.Trigger(context=context, **trigger_definition)
self.operationengine_api.create_trigger(context, trigger)
trigger.create()
except exception.Invalid as ex:
raise exc.HTTPBadRequest(explanation=ex.msg)
except Exception as ex:
self._raise_unknown_exception(ex)
return self._view_builder.detail(req, trigger)
def delete(self, req, id):
"""Delete a trigger."""
LOG.debug('Delete trigger(%s) start', id)
context = req.environ['smaug.context']
trigger = self._get_trigger_by_id(context, id)
check_policy(context, 'delete', trigger)
try:
self.operationengine_api.delete_trigger(context, id)
except exception.TriggerNotFound as ex:
pass
except exception.DeleteTriggerNotAllowed as ex:
raise exc.HTTPBadRequest(explanation=ex.msg)
except Exception as ex:
self._raise_unknown_exception(ex)
trigger.destroy()
def update(self, req, id, body):
"""Update a trigger"""
LOG.debug('Update trigger(%s) start', id)
context = req.environ['smaug.context']
trigger = self._get_trigger_by_id(context, id)
check_policy(context, 'update', trigger)
trigger_info = body['trigger_info']
trigger_name = trigger_info.get("name", None)
trigger_property = trigger_info.get("properties", None)
if trigger_name:
self.validate_name_and_description(trigger_info)
trigger.name = trigger_name
if trigger_property:
try:
trigger.properties = trigger_property
self.operationengine_api.update_trigger(context, trigger)
except exception.InvalidInput as ex:
raise exc.HTTPBadRequest(explanation=ex.msg)
except (exception.TriggerNotFound, Exception) as ex:
self._raise_unknown_exception(ex)
try:
trigger.save()
except Exception as ex:
self._raise_unknown_exception(ex)
return self._view_builder.detail(req, trigger)
def show(self, req, id):
"""Return data about the given trigger."""
LOG.debug('Get trigger(%s) start', id)
context = req.environ['smaug.context']
trigger = self._get_trigger_by_id(context, id)
check_policy(context, 'get', trigger)
return self._view_builder.detail(req, trigger)
def index(self, req):
"""Returns a list of triggers, transformed through view builder."""
context = req.environ['smaug.context']
check_policy(context, 'list')
params = req.params.copy()
LOG.debug('List triggers start, params=%s', params)
marker, limit, offset = common.get_pagination_params(params)
sort_keys, sort_dirs = common.get_sort_params(params)
filters = params
valid_filters = ["all_tenants", "name", "type", "properties"]
utils.remove_invalid_filter_options(context, filters, valid_filters)
utils.check_filters(filters)
all_tenants = utils.get_bool_param("all_tenants", filters)
if not (context.is_admin and all_tenants):
filters["project_id"] = context.project_id
try:
triggers = objects.TriggerList.get_by_filters(
context, filters, limit, marker, sort_keys, sort_dirs)
except Exception as ex:
self._raise_unknown_exception(ex)
return self._view_builder.detail_list(req, triggers)
def _get_trigger_by_id(self, context, id):
if not uuidutils.is_uuid_like(id):
msg = _("Invalid trigger id provided.")
raise exc.HTTPBadRequest(explanation=msg)
try:
trigger = objects.Trigger.get_by_id(context, id)
except exception.TriggerNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg)
except Exception as ex:
self._raise_unknown_exception(ex)
return trigger
def _raise_unknown_exception(self, exception_instance):
value = exception_instance.msg if isinstance(
exception_instance, exception.SmaugException) else type(
exception_instance)
msg = (_('Unexpected API Error. Please report this at '
'http://bugs.launchpad.net/smaug/ and attach the '
'Smaug API log if possible.\n%s') % value)
raise exc.HTTPInternalServerError(explanation=msg)
def create_resource():
return wsgi.Resource(TriggersController())

View File

@ -178,6 +178,18 @@ def trigger_delete(context, id):
return IMPL.trigger_delete(context, id)
def trigger_get_all_by_filters_sort(context, filters, limit=None,
marker=None, sort_keys=None,
sort_dirs=None):
"""Get all triggers that match all filters sorted by multiple keys.
sort_keys and sort_dirs must be a list of strings.
"""
return IMPL.trigger_get_all_by_filters_sort(
context, filters, limit=limit, marker=marker,
sort_keys=sort_keys, sort_dirs=sort_dirs)
###################

View File

@ -14,6 +14,7 @@
import functools
import re
import six
import sys
import threading
import time
@ -395,6 +396,30 @@ def trigger_delete(context, id):
trigger_ref.delete(session=session)
def _trigger_list_process_filters(query, filters):
exact_match_filter_names = ['project_id', 'type']
query = _list_common_process_exact_filter(models.Trigger, query, filters,
exact_match_filter_names)
regex_match_filter_names = ['name', 'properties']
query = _list_common_process_regex_filter(models.Trigger, query, filters,
regex_match_filter_names)
return query
def trigger_get_all_by_filters_sort(context, filters, limit=None, marker=None,
sort_keys=None, sort_dirs=None):
session = get_session()
with session.begin():
query = _generate_paginate_query(context, session, marker, limit,
sort_keys, sort_dirs, filters,
paginate_type=models.Trigger,
use_model=True)
return query.all() if query else []
###################
@ -990,21 +1015,105 @@ def _process_restore_filters(query, filters):
return None
query = query.filter_by(**filters)
return query
###############################
@require_context
def _list_common_get_query(context, model, session=None):
return model_query(context, model, session=session)
def _list_common_process_exact_filter(model, query, filters, legal_keys):
"""Applies exact match filtering to a query.
:param model: model to apply filters to
:param query: query to apply filters to
:param filters: dictionary of filters; values that are lists,
tuples, sets, or frozensets cause an 'IN' test to
be performed, while exact matching ('==' operator)
is used for other values
:param legal_keys: list of keys to apply exact filtering to
:returns: the updated query.
"""
filter_dict = {}
for key in legal_keys:
if key not in filters:
continue
value = filters.get(key)
if isinstance(value, (list, tuple, set, frozenset)):
if not value:
return None # empty IN-predicate; short circuit
# Looking for values in a list; apply to query directly
column_attr = getattr(model, key)
query = query.filter(column_attr.in_(value))
else:
# OK, simple exact match; save for later
filter_dict[key] = value
# Apply simple exact matches
if filter_dict:
query = query.filter_by(**filter_dict)
return query
def _list_common_process_regex_filter(model, query, filters, legal_keys):
"""Applies regular expression filtering to a query.
:param model: model to apply filters to
:param query: query to apply filters to
:param filters: dictionary of filters with regex values
:param legal_keys: list of keys to apply regex filtering to
:returns: the updated query.
"""
def _get_regexp_op_for_connection(db_connection):
db_string = db_connection.split(':')[0].split('+')[0]
regexp_op_map = {
'postgresql': '~',
'mysql': 'REGEXP',
'sqlite': 'REGEXP'
}
return regexp_op_map.get(db_string, 'LIKE')
db_regexp_op = _get_regexp_op_for_connection(CONF.database.connection)
for key in legal_keys:
if key not in filters:
continue
value = filters[key]
if not isinstance(value, six.string_types):
continue
column_attr = getattr(model, key)
if db_regexp_op == 'LIKE':
query = query.filter(column_attr.op(db_regexp_op)(
u'%' + value + u'%'))
else:
query = query.filter(column_attr.op(db_regexp_op)(
value))
return query
PAGINATION_HELPERS = {
models.Plan: (_plan_get_query, _process_plan_filters, _plan_get),
models.Restore: (_restore_get_query, _process_restore_filters,
_restore_get)
_restore_get),
models.Trigger: (_list_common_get_query, _trigger_list_process_filters,
_trigger_get),
}
###############################
def _generate_paginate_query(context, session, marker, limit, sort_keys,
sort_dirs, filters, offset=None,
paginate_type=models.Plan):
paginate_type=models.Plan, use_model=False):
"""Generate the query to include the filters and the paginate options.
Returns a query with sorting / pagination criteria added or None
@ -1032,7 +1141,10 @@ def _generate_paginate_query(context, session, marker, limit, sort_keys,
sort_keys, sort_dirs = process_sort_params(sort_keys,
sort_dirs,
default_dir='desc')
query = get_query(context, session=session)
if use_model:
query = get_query(context, session=session, model=paginate_type)
else:
query = get_query(context, session=session)
if filters:
query = process_filters(query, filters)

View File

@ -16,8 +16,6 @@ from oslo_serialization import jsonutils
from oslo_versionedobjects import fields
from smaug import db
from smaug import exception
from smaug.i18n import _
from smaug.objects import base
CONF = cfg.CONF
@ -69,10 +67,6 @@ class Trigger(base.SmaugPersistentObject, base.SmaugObject,
@base.remotable
def create(self):
if self.obj_attr_is_set('id'):
raise exception.ObjectActionError(action='create',
reason=_('already created'))
updates = self.smaug_obj_get_changes()
self._convert_properties_to_db_format(updates)
db_trigger = db.trigger_create(self._context, updates)

View File

@ -0,0 +1,148 @@
# 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 webob import exc
from smaug.api.v1 import triggers as trigger_api
from smaug import context
from smaug import exception
from smaug.i18n import _
from smaug.tests import base
from smaug.tests.unit.api import fakes
class FakeRemoteOperationApi(object):
def create_trigger(self, context, trigger):
if trigger.type not in ['time']:
msg = (_("Invalid trigger type:%s") % trigger.type)
raise exception.InvalidInput(msg)
if trigger.properties['format'] not in ['crontab']:
msg = (_("Invalid trigger time format type"))
raise exception.InvalidInput(msg)
def delete_trigger(self, context, trigger_id):
pass
def update_trigger(self, context, trigger):
pass
class TriggerApiTest(base.TestCase):
def setUp(self):
super(TriggerApiTest, self).setUp()
self.controller = trigger_api.TriggersController()
self.controller.operationengine_api = FakeRemoteOperationApi()
self.ctxt = context.RequestContext('admin', 'fakeproject',
True)
self.req = fakes.HTTPRequest.blank('/v1/triggers')
self.default_create_trigger_param = {
"name": "123",
"type": "time",
"properties": {
"format": "crontab",
"pattern": "* * * * *"
},
}
def test_create_trigger_InvalidBody(self):
self.assertRaises(exc.HTTPUnprocessableEntity,
self.controller.create,
self.req, {})
def test_create_trigger_InvalidName(self):
body = self._get_create_trigger_request_body()
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
def test_create_trigger_invalid_trigger_type(self):
param = self.default_create_trigger_param.copy()
param['type'] = "123"
body = self._get_create_trigger_request_body(param)
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
def test_create_trigger_invalid_trigger_formt_type(self):
param = self.default_create_trigger_param.copy()
param['properties']['format'] = "123"
body = self._get_create_trigger_request_body(param)
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
def test_create_trigger(self):
name = 'every minutes'
param = self.default_create_trigger_param.copy()
param['name'] = name
body = self._get_create_trigger_request_body(param)
trigger = self.controller.create(self.req, body)
self.assertEqual(name, trigger['trigger_info']['name'])
def test_delete_trigger(self):
trigger = self._create_one_trigger()
self.controller.delete(self.req, trigger['trigger_info']['id'])
self.assertRaises(exc.HTTPNotFound,
self.controller.show,
self.req,
trigger['trigger_info']['id'])
def test_update_trigger(self):
trigger = self._create_one_trigger()
name = 'every minutes'
param = self.default_create_trigger_param.copy()
param['name'] = name
param['properties']['window'] = 10
body = self._get_create_trigger_request_body(param)
trigger1 = self.controller.update(
self.req, trigger['trigger_info']['id'], body)
self.assertEqual(name, trigger1['trigger_info']['name'])
self.assertEqual(10, int(
trigger1['trigger_info']['properties']['window']))
def test_show_trigger_not_exist(self):
self.assertRaises(exc.HTTPNotFound,
self.controller.show,
self.req,
'2a9ce1f3-cc1a-4516-9435-0ebb13caa398')
def test_show_trigger_invalid_id(self):
self.assertRaises(exc.HTTPBadRequest,
self.controller.show,
self.req, 1)
def test_show_trigger(self):
trigger = self._create_one_trigger()
trigger1 = self.controller.show(self.req,
trigger['trigger_info']['id'])
self.assertEqual(trigger['trigger_info']['id'],
trigger1['trigger_info']['id'])
def test_list_trigger(self):
trigger = self._create_one_trigger()
triggers = self.controller.index(self.req)
for item in triggers['triggers']:
if item['id'] == trigger['trigger_info']['id']:
self.assertTrue(1)
self.assertFalse(0)
def _create_one_trigger(self):
param = self.default_create_trigger_param.copy()
body = self._get_create_trigger_request_body(param)
return self.controller.create(self.req, body)
def _get_create_trigger_request_body(self, param={}):
return {"trigger_info": param}

View File

@ -6,5 +6,10 @@
"admin_api": "is_admin:True",
"plan:create": "",
"plan:delete": "rule:admin_or_owner"
"plan:delete": "rule:admin_or_owner",
"trigger:create": "",
"trigger:delete": "rule:admin_or_owner",
"trigger:get": "rule:admin_or_owner",
"trigger:list": ""
}