Implement RestAPIs of Scheduled Operation.

Includes create/delete/get/list APIs.

Change-Id: I5c2578a810cbbaf65847cdd048a6df6bb91176de
Closes-Bug: #1552130
This commit is contained in:
zengchen 2016-03-02 16:28:40 +08:00
parent d1d69da052
commit d54d6835d1
9 changed files with 521 additions and 70 deletions

View File

@ -30,5 +30,10 @@
"trigger:delete": "rule:admin_or_owner",
"trigger:update": "rule:admin_or_owner",
"trigger:get": "rule:admin_or_owner",
"trigger:list": ""
"trigger:list": "",
"scheduled_operation:create": "",
"scheduled_operation:delete": "rule:admin_or_owner",
"scheduled_operation:get": "rule:admin_or_owner",
"scheduled_operation:list": ""
}

View File

@ -83,6 +83,6 @@ class APIRouter(wsgi_common.Router):
member={'action': 'POST'})
mapper.resource("scheduled_operation", "scheduled_operations",
controller=scheduled_operation_resources,
collection={'detail': 'GET'},
collection={},
member={'action': 'POST'})
super(APIRouter, self).__init__(mapper)

View File

@ -10,91 +10,255 @@
# License for the specific language governing permissions and limitations
# under the License.
"""The ScheduledOperations api."""
"""The scheduled operations api."""
from oslo_log import log as logging
import webob
from oslo_utils import uuidutils
from webob import exc
from smaug.api import common
from smaug.api.openstack import wsgi
from smaug.i18n import _LI
from smaug import exception
from smaug.i18n import _
from smaug import objects
from smaug.operationengine import api as operationengine_api
from smaug.operationengine import operation_manager
from smaug import policy
from smaug import utils
LOG = logging.getLogger(__name__)
OPERATION_MANAGER = operation_manager.OperationManager()
class ScheduledOperationsController(wsgi.Controller):
"""The ScheduledOperations API controller for the OpenStack API."""
def check_policy(context, action, target_obj=None):
_action = 'scheduled_operation:%s' % action
policy.enforce(context, _action, target_obj)
class ScheduledOperationViewBuilder(common.ViewBuilder):
"""Model a server API response as a python dictionary."""
_collection_name = "scheduled_operations"
def detail(self, request, operation):
"""Detailed view of a single scheduled operation."""
operation_ref = {
'scheduled_operation': {
'id': operation.get('id'),
'name': operation.get('name'),
'operation_type': operation.get('operation_type'),
'project_id': operation.get('project_id'),
'trigger_id': operation.get('trigger_id'),
'operation_definition': operation.get('operation_definition'),
}
}
return operation_ref
def detail_list(self, request, operations):
"""Detailed view of a list of operations."""
return self._list_view(self.detail, request, operations)
def _list_view(self, func, request, operations):
operations_list = [func(request, item)['scheduled_operation']
for item in operations]
operations_links = self._get_collection_links(request,
operations,
self._collection_name,
)
ret = {'operations': operations_list}
if operations_links:
ret['operations_links'] = operations_links
return ret
class ScheduledOperationController(wsgi.Controller):
"""The Scheduled Operation API controller for the OpenStack API."""
_view_builder_class = ScheduledOperationViewBuilder
def __init__(self):
self.operationengine_api = operationengine_api.API()
super(ScheduledOperationsController, self).__init__()
def show(self, req, id):
"""Return data about the given scheduled_operation."""
context = req.environ['smaug.context']
LOG.info(_LI("Show ScheduledOperation with id: %s"), id,
context=context)
# TODO(chenying)
return {'Smaug': "ScheduledOperations show."}
def delete(self, req, id):
"""Delete a scheduled_operation."""
context = req.environ['smaug.context']
LOG.info(_LI("Delete ScheduledOperations with id: %s"), id,
context=context)
# TODO(chenying)
return webob.Response(status_int=202)
def index(self, req):
"""Returns a summary list of ScheduledOperations."""
# TODO(chenying)
return {'scheduled_operation': "ScheduledOperations index."}
def detail(self, req):
"""Returns a detailed list of ScheduledOperations."""
# TODO(chenying)
return {'scheduled_operation': "ScheduledOperations detail."}
super(ScheduledOperationController, self).__init__()
def create(self, req, body):
"""Creates a new ScheduledOperation."""
"""Creates a new scheduled operation."""
LOG.debug('Create ScheduledOperations request body: %s', body)
context = req.environ['smaug.context']
request_spec = {'resource': 'ScheduledOperations',
'method': 'create'}
self.operationengine_api.create_scheduled_operation(context,
request_spec)
LOG.debug('Create ScheduledOperations request context: %s', context)
LOG.debug('Create scheduled operation start')
# TODO(chenying)
return {'scheduled_operation': "Create a ScheduledOperation."}
def update(self, req, id, body):
"""Update a scheduled_operation."""
context = req.environ['smaug.context']
if not body:
if not self.is_valid_body(body, 'scheduled_operation'):
raise exc.HTTPUnprocessableEntity()
LOG.debug('Create a scheduled operation, request body: %s', body)
if 'scheduled_operation' not in body:
raise exc.HTTPUnprocessableEntity()
context = req.environ['smaug.context']
check_policy(context, 'create')
operation_info = body['scheduled_operation']
scheduled_operation = body['scheduled_operation']
name = operation_info.get("name", None)
operation_type = operation_info.get("operation_type", None)
operation_definition = operation_info.get(
"operation_definition", None)
if name is None:
msg = _("Operation name or type or definition is not provided.")
raise exc.HTTPBadRequest(explanation=msg)
LOG.info(_LI("Update ScheduledOperation : %s"), scheduled_operation,
context=context)
self.validate_name_and_description(operation_info)
return {'scheduled_operation': "Update a ScheduledOperation."}
try:
OPERATION_MANAGER.check_operation_definition(
operation_type, operation_definition)
except exception.Invalid as ex:
raise exc.HTTPBadRequest(explanation=ex.msg)
except Exception as ex:
self._raise_unknown_exception(ex)
trigger_id = operation_info.get("trigger_id", None)
trigger = self._get_trigger_by_id(context, trigger_id)
if context.project_id != trigger.project_id:
msg = _("Invalid trigger id provided.")
raise exc.HTTPBadRequest(explanation=msg)
operation_obj = {
'name': operation_info.get('name', None),
'operation_type': operation_type,
'project_id': context.project_id,
'trigger_id': trigger_id,
'operation_definition': operation_definition,
}
try:
operation = objects.ScheduledOperation(context=context,
**operation_obj)
operation.create()
except Exception as ex:
self._raise_unknown_exception(ex)
try:
self._create_scheduled_operation(context, operation.id,
trigger_id)
except Exception:
try:
operation.destroy()
except Exception:
pass
raise
return self._view_builder.detail(req, operation)
def delete(self, req, id):
"""Delete a scheduled operation."""
LOG.debug('Delete scheduled operation(%s) start', id)
context = req.environ['smaug.context']
operation = self._get_operation_by_id(context, id, ['trigger'])
trigger = operation.trigger
check_policy(context, 'delete', operation)
try:
self.operationengine_api.delete_scheduled_operation(
context, id, trigger.id)
except (exception.ScheduledOperationStateNotFound,
exception.TriggerNotFound,
Exception) as ex:
self._raise_unknown_exception(ex)
operation.destroy()
def show(self, req, id):
"""Return data about the given operation."""
LOG.debug('Get scheduled operation(%s) start', id)
context = req.environ['smaug.context']
operation = self._get_operation_by_id(context, id)
check_policy(context, 'get', operation)
return self._view_builder.detail(req, operation)
def index(self, req):
"""Returns a list of operations, transformed through view builder."""
context = req.environ['smaug.context']
check_policy(context, 'list')
params = req.params.copy()
LOG.debug('List scheduled operation 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", "operation_type",
"trigger_id", "operation_definition"]
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:
operations = objects.ScheduledOperationList.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, operations)
def _get_operation_by_id(self, context, id, expect_attrs=[]):
if not uuidutils.is_uuid_like(id):
msg = _("Invalid operation id provided.")
raise exc.HTTPBadRequest(explanation=msg)
try:
operation = objects.ScheduledOperation.get_by_id(
context, id, expect_attrs)
except exception.ScheduledOperationNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg)
except Exception as ex:
self._raise_unknown_exception(ex)
return operation
def _get_trigger_by_id(self, context, trigger_id):
if not uuidutils.is_uuid_like(trigger_id):
msg = _("Invalid trigger id provided.")
raise exc.HTTPBadRequest(explanation=msg)
try:
trigger = objects.Trigger.get_by_id(context, trigger_id)
except exception.NotFound as ex:
raise exc.HTTPNotFound(explanation=ex.msg)
except Exception as ex:
self._raise_unknown_exception(ex)
return trigger
def _create_scheduled_operation(self, context, operation_id, trigger_id):
try:
self.operationengine_api.create_scheduled_operation(
context, operation_id, trigger_id)
except (exception.InvalidInput,
exception.TriggerIsInvalid) as ex:
raise exc.HTTPBadRequest(explanation=ex.msg)
except (exception.TriggerNotFound, Exception) as ex:
self._raise_unknown_exception(ex)
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(ScheduledOperationsController())
return wsgi.Resource(ScheduledOperationController())

View File

@ -131,13 +131,23 @@ class TriggersController(wsgi.Controller):
check_policy(context, 'delete', trigger)
try:
operations = objects.ScheduledOperationList.get_by_filters(
context, {"trigger_id": id}, limit=1)
except Exception as ex:
self._raise_unknown_exception(ex)
if operations:
msg = _("There are more than one scheduled operations binded "
"with this trigger, please delete them first")
raise exc.HTTPMethodNotAllowed(explanation=msg)
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:
except (exception.DeleteTriggerNotAllowed,
Exception) as ex:
self._raise_unknown_exception(ex)
trigger.destroy()

View File

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

View File

@ -476,6 +476,35 @@ def scheduled_operation_delete(context, id):
session.flush()
def _scheduled_operation_list_process_filters(query, filters):
exact_match_filter_names = ['project_id', 'operation_type', 'trigger_id']
query = _list_common_process_exact_filter(
models.ScheduledOperation, query, filters,
exact_match_filter_names)
regex_match_filter_names = ['name', 'operation_definition']
query = _list_common_process_regex_filter(
models.ScheduledOperation, query, filters,
regex_match_filter_names)
return query
def scheduled_operation_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.ScheduledOperation,
use_model=True)
return query.all() if query else []
###################
@ -1105,6 +1134,9 @@ PAGINATION_HELPERS = {
_restore_get),
models.Trigger: (_list_common_get_query, _trigger_list_process_filters,
_trigger_get),
models.ScheduledOperation: (_list_common_get_query,
_scheduled_operation_list_process_filters,
_scheduled_operation_get),
}
@ -1153,7 +1185,7 @@ def _generate_paginate_query(context, session, marker, limit, sort_keys,
marker_object = None
if marker is not None:
marker_object = get(context, marker, session)
marker_object = get(context, marker, session=session)
return sqlalchemyutils.paginate_query(query, paginate_type, limit,
sort_keys,

View File

@ -0,0 +1,198 @@
# 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 scheduled_operations as operation_api
from smaug.api.v1 import triggers as trigger_api
from smaug import context
from smaug import exception
from smaug.tests import base
from smaug.tests.unit.api import fakes
from smaug.tests.unit.api.v1 import test_triggers
class FakeRemoteOperationApi(object):
def __init__(self):
self._create_operation_exception = None
self._delete_operation_exception = None
def create_scheduled_operation(self, context, operation_id, trigger_id):
if self._create_operation_exception:
raise self._create_operation_exception
def delete_scheduled_operation(self, context, operation_id, trigger_id):
if self._delete_operation_exception:
raise self._delete_operation_exception
class ScheduledOperationApiTest(base.TestCase):
def setUp(self):
super(ScheduledOperationApiTest, self).setUp()
self.remote_operation_api = FakeRemoteOperationApi()
self.controller = operation_api.ScheduledOperationController()
self.controller.operationengine_api = self.remote_operation_api
self.ctxt = context.RequestContext('admin', 'fakeproject', True)
self.req = fakes.HTTPRequest.blank('/v1/scheduled_operations')
trigger = self._create_trigger()
self.default_create_operation_param = {
"name": "123",
"operation_type": "protect",
"trigger_id": trigger['trigger_info']['id'],
"operation_definition": {
"plan_id": ""
},
}
def test_create_operation_InvalidBody(self):
self.assertRaises(exc.HTTPUnprocessableEntity,
self.controller.create,
self.req, {})
def test_create_operation_InvalidName(self):
body = self._get_create_operation_request_body()
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
def test_create_operation_invalid_operation_type(self):
param = self.default_create_operation_param.copy()
param['operation_type'] = "123"
body = self._get_create_operation_request_body(param)
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
def test_create_operation_invalid_trigger(self):
param = self.default_create_operation_param.copy()
param['trigger_id'] = 123
body = self._get_create_operation_request_body(param)
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
def test_create_operation_invalid_operation_definition(self):
param = self.default_create_operation_param.copy()
del param['operation_definition']['plan_id']
body = self._get_create_operation_request_body(param)
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
def test_create_operation_recieve_invalid_except(self):
self.remote_operation_api._create_operation_exception =\
exception.TriggerIsInvalid(trigger_id=None)
param = self.default_create_operation_param.copy()
body = self._get_create_operation_request_body(param)
self.assertRaises(exc.HTTPBadRequest,
self.controller.create,
self.req, body)
self.remote_operation_api._create_operation_exception = None
def test_create_operation_recieve_unknown_except(self):
self.remote_operation_api._create_operation_exception =\
exception.TriggerNotFound(id=None)
param = self.default_create_operation_param.copy()
body = self._get_create_operation_request_body(param)
self.assertRaises(exc.HTTPInternalServerError,
self.controller.create,
self.req, body)
self.remote_operation_api._create_operation_exception = None
def test_create_operation(self):
name = 'my protect'
param = self.default_create_operation_param.copy()
param['name'] = name
body = self._get_create_operation_request_body(param)
operation = self.controller.create(self.req, body)
self.assertEqual(name, operation['scheduled_operation']['name'])
def test_delete_operation_recieve_NotFound_except(self):
self.remote_operation_api._delete_operation_exception =\
exception.ScheduledOperationStateNotFound(op_id=None)
operation = self._create_one_operation()
self.assertRaises(exc.HTTPInternalServerError,
self.controller.delete,
self.req,
operation['scheduled_operation']['id'])
self.remote_operation_api._delete_operation_exception = None
def test_delete_operation(self):
operation = self._create_one_operation()
self.controller.delete(self.req,
operation['scheduled_operation']['id'])
self.assertRaises(exc.HTTPNotFound,
self.controller.show,
self.req,
operation['scheduled_operation']['id'])
def test_show_operation_not_exist(self):
self.assertRaises(exc.HTTPNotFound,
self.controller.show,
self.req,
'2a9ce1f3-cc1a-4516-9435-0ebb13caa398')
def test_show_operation_invalid_id(self):
self.assertRaises(exc.HTTPBadRequest,
self.controller.show,
self.req, 1)
def test_show_operation(self):
operation = self._create_one_operation()
operation1 = self.controller.show(
self.req, operation['scheduled_operation']['id'])
self.assertEqual(operation['scheduled_operation']['id'],
operation1['scheduled_operation']['id'])
def test_list_operation(self):
operation = self._create_one_operation()
operations = self.controller.index(self.req)
for item in operations['operations']:
if item['id'] == operation['scheduled_operation']['id']:
self.assertTrue(1)
self.assertFalse(0)
def _create_one_operation(self):
param = self.default_create_operation_param.copy()
body = self._get_create_operation_request_body(param)
return self.controller.create(self.req, body)
def _get_create_operation_request_body(self, param={}):
return {"scheduled_operation": param}
def _create_trigger(self):
create_trigger_param = {
"trigger_info": {
"name": "123",
"type": "time",
"properties": {
"format": "crontab",
"pattern": "* * * * *"
},
}
}
controller = trigger_api.TriggersController()
controller.operationengine_api = test_triggers.\
FakeRemoteOperationApi()
req = fakes.HTTPRequest.blank('/v1/triggers')
return controller.create(req, create_trigger_param)

View File

@ -16,6 +16,7 @@ from smaug.api.v1 import triggers as trigger_api
from smaug import context
from smaug import exception
from smaug.i18n import _
from smaug import objects
from smaug.tests import base
from smaug.tests.unit.api import fakes
@ -89,6 +90,16 @@ class TriggerApiTest(base.TestCase):
trigger = self.controller.create(self.req, body)
self.assertEqual(name, trigger['trigger_info']['name'])
def test_delete_trigger_binded_with_operation(self):
trigger = self._create_one_trigger()
trigger_id = trigger['trigger_info']['id']
self._create_scheduled_operation(trigger_id)
self.assertRaises(exc.HTTPMethodNotAllowed,
self.controller.delete,
self.req,
trigger_id)
def test_delete_trigger(self):
trigger = self._create_one_trigger()
self.controller.delete(self.req, trigger['trigger_info']['id'])
@ -146,3 +157,17 @@ class TriggerApiTest(base.TestCase):
def _get_create_trigger_request_body(self, param={}):
return {"trigger_info": param}
def _create_scheduled_operation(self, trigger_id):
operation_info = {
"name": "123",
"operation_type": "protect",
"project_id": "123",
"trigger_id": trigger_id,
"operation_definition": {
"plan_id": ""
},
}
operation = objects.ScheduledOperation(self.ctxt, **operation_info)
operation.create()
return operation

View File

@ -11,5 +11,10 @@
"trigger:create": "",
"trigger:delete": "rule:admin_or_owner",
"trigger:get": "rule:admin_or_owner",
"trigger:list": ""
"trigger:list": "",
"scheduled_operation:create": "",
"scheduled_operation:delete": "rule:admin_or_owner",
"scheduled_operation:get": "rule:admin_or_owner",
"scheduled_operation:list": ""
}