Port share type extensions to core API
Changes: - Register share type APIs as core API. - Remove extensions code for share types. - Leave rename of API 'post' data for future update which will be done with bump of microversion after port of all extensions to core API. Partially implements bp ext-to-core Change-Id: I334cc0fa8e69c2f9b5226f247149a0b9b748dd60
This commit is contained in:
parent
9912d296ec
commit
0cb695fd54
@ -54,12 +54,17 @@
|
|||||||
"share_type:index": "rule:default",
|
"share_type:index": "rule:default",
|
||||||
"share_type:show": "rule:default",
|
"share_type:show": "rule:default",
|
||||||
"share_type:default": "rule:default",
|
"share_type:default": "rule:default",
|
||||||
|
"share_type:create": "rule:admin_api",
|
||||||
|
"share_type:delete": "rule:admin_api",
|
||||||
|
"share_type:add_project_access": "rule:admin_api",
|
||||||
|
"share_type:list_project_access": "rule:admin_api",
|
||||||
|
"share_type:remove_project_access": "rule:admin_api",
|
||||||
|
|
||||||
"share_extension:types_manage": "rule:admin_api",
|
"share_types_extra_spec:create": "rule:admin_api",
|
||||||
"share_extension:types_extra_specs": "rule:admin_api",
|
"share_types_extra_spec:update": "rule:admin_api",
|
||||||
"share_extension:share_type_access": "",
|
"share_types_extra_spec:show": "rule:admin_api",
|
||||||
"share_extension:share_type_access:addProjectAccess": "rule:admin_api",
|
"share_types_extra_spec:index": "rule:admin_api",
|
||||||
"share_extension:share_type_access:removeProjectAccess": "rule:admin_api",
|
"share_types_extra_spec:delete": "rule:admin_api",
|
||||||
|
|
||||||
"security_service:create": "rule:default",
|
"security_service:create": "rule:default",
|
||||||
"security_service:delete": "rule:default",
|
"security_service:delete": "rule:default",
|
||||||
|
@ -1,163 +0,0 @@
|
|||||||
#
|
|
||||||
# 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 share type access extension."""
|
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
import six
|
|
||||||
import webob
|
|
||||||
|
|
||||||
|
|
||||||
from manila.api import extensions
|
|
||||||
from manila.api.openstack import wsgi
|
|
||||||
from manila import exception
|
|
||||||
from manila.i18n import _
|
|
||||||
from manila.share import share_types
|
|
||||||
|
|
||||||
|
|
||||||
soft_authorize = extensions.soft_extension_authorizer('share',
|
|
||||||
'share_type_access')
|
|
||||||
authorize = extensions.extension_authorizer('share', 'share_type_access')
|
|
||||||
|
|
||||||
|
|
||||||
def _marshall_share_type_access(share_type):
|
|
||||||
rval = []
|
|
||||||
for project_id in share_type['projects']:
|
|
||||||
rval.append({'share_type_id': share_type['id'],
|
|
||||||
'project_id': project_id})
|
|
||||||
|
|
||||||
return {'share_type_access': rval}
|
|
||||||
|
|
||||||
|
|
||||||
class ShareTypeAccessController(object):
|
|
||||||
"""The share type access API controller for the OpenStack API."""
|
|
||||||
|
|
||||||
def index(self, req, type_id):
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
authorize(context)
|
|
||||||
|
|
||||||
try:
|
|
||||||
share_type = share_types.get_share_type(
|
|
||||||
context, type_id, expected_fields=['projects'])
|
|
||||||
except exception.ShareTypeNotFound:
|
|
||||||
explanation = _("Share type %s not found.") % type_id
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=explanation)
|
|
||||||
|
|
||||||
if share_type['is_public']:
|
|
||||||
expl = _("Access list not available for public share types.")
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=expl)
|
|
||||||
|
|
||||||
return _marshall_share_type_access(share_type)
|
|
||||||
|
|
||||||
|
|
||||||
class ShareTypeActionController(wsgi.Controller):
|
|
||||||
"""The share type access API controller for the OpenStack API."""
|
|
||||||
|
|
||||||
def _check_body(self, body, action_name):
|
|
||||||
if not self.is_valid_body(body, action_name):
|
|
||||||
raise webob.exc.HTTPBadRequest()
|
|
||||||
access = body[action_name]
|
|
||||||
project = access.get('project')
|
|
||||||
if not uuidutils.is_uuid_like(project):
|
|
||||||
msg = _("Bad project format: "
|
|
||||||
"project is not in proper format (%s)") % project
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
def _extend_share_type(self, share_type_rval, share_type_ref):
|
|
||||||
if share_type_ref:
|
|
||||||
key = "%s:is_public" % (Share_type_access.alias)
|
|
||||||
share_type_rval[key] = share_type_ref.get('is_public', True)
|
|
||||||
|
|
||||||
@wsgi.extends
|
|
||||||
def show(self, req, resp_obj, id):
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
if soft_authorize(context):
|
|
||||||
share_type = req.get_db_share_type(id)
|
|
||||||
self._extend_share_type(resp_obj.obj['share_type'], share_type)
|
|
||||||
|
|
||||||
@wsgi.extends
|
|
||||||
def index(self, req, resp_obj):
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
if soft_authorize(context):
|
|
||||||
for share_type_rval in list(resp_obj.obj['share_types']):
|
|
||||||
type_id = share_type_rval['id']
|
|
||||||
share_type = req.get_db_share_type(type_id)
|
|
||||||
self._extend_share_type(share_type_rval, share_type)
|
|
||||||
|
|
||||||
@wsgi.extends(action='create')
|
|
||||||
def create(self, req, body, resp_obj):
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
if soft_authorize(context):
|
|
||||||
type_id = resp_obj.obj['share_type']['id']
|
|
||||||
share_type = req.get_db_share_type(type_id)
|
|
||||||
self._extend_share_type(resp_obj.obj['share_type'], share_type)
|
|
||||||
|
|
||||||
@wsgi.action('addProjectAccess')
|
|
||||||
def _addProjectAccess(self, req, id, body):
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
authorize(context, action="addProjectAccess")
|
|
||||||
self._check_body(body, 'addProjectAccess')
|
|
||||||
project = body['addProjectAccess']['project']
|
|
||||||
|
|
||||||
try:
|
|
||||||
share_type = share_types.get_share_type(context, id)
|
|
||||||
|
|
||||||
if share_type['is_public']:
|
|
||||||
msg = _("You cannot add project to public share_type.")
|
|
||||||
raise webob.exc.HTTPForbidden(explanation=msg)
|
|
||||||
|
|
||||||
except exception.ShareTypeNotFound as err:
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
|
||||||
|
|
||||||
try:
|
|
||||||
share_types.add_share_type_access(context, id, project)
|
|
||||||
except exception.ShareTypeAccessExists as err:
|
|
||||||
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
|
||||||
|
|
||||||
return webob.Response(status_int=202)
|
|
||||||
|
|
||||||
@wsgi.action('removeProjectAccess')
|
|
||||||
def _removeProjectAccess(self, req, id, body):
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
authorize(context, action="removeProjectAccess")
|
|
||||||
self._check_body(body, 'removeProjectAccess')
|
|
||||||
project = body['removeProjectAccess']['project']
|
|
||||||
|
|
||||||
try:
|
|
||||||
share_types.remove_share_type_access(context, id, project)
|
|
||||||
except (exception.ShareTypeNotFound,
|
|
||||||
exception.ShareTypeAccessNotFound) as err:
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
|
||||||
return webob.Response(status_int=202)
|
|
||||||
|
|
||||||
|
|
||||||
class Share_type_access(extensions.ExtensionDescriptor):
|
|
||||||
"""share type access support."""
|
|
||||||
|
|
||||||
name = "ShareTypeAccess"
|
|
||||||
alias = "os-share-type-access"
|
|
||||||
updated = "2015-03-02T00:00:00Z"
|
|
||||||
|
|
||||||
def get_resources(self):
|
|
||||||
resources = []
|
|
||||||
res = extensions.ResourceExtension(
|
|
||||||
Share_type_access.alias,
|
|
||||||
ShareTypeAccessController(),
|
|
||||||
parent=dict(member_name='type', collection_name='types'))
|
|
||||||
resources.append(res)
|
|
||||||
return resources
|
|
||||||
|
|
||||||
def get_controller_extensions(self):
|
|
||||||
controller = ShareTypeActionController()
|
|
||||||
extension = extensions.ControllerExtension(self, 'types', controller)
|
|
||||||
return [extension]
|
|
@ -1,132 +0,0 @@
|
|||||||
# Copyright (c) 2011 OpenStack Foundation
|
|
||||||
#
|
|
||||||
# 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 share types manage extension."""
|
|
||||||
|
|
||||||
import six
|
|
||||||
import webob
|
|
||||||
|
|
||||||
from manila.api import extensions
|
|
||||||
from manila.api.openstack import wsgi
|
|
||||||
from manila.api.views import types as views_types
|
|
||||||
from manila import exception
|
|
||||||
from manila.i18n import _
|
|
||||||
from manila import rpc
|
|
||||||
from manila.share import share_types
|
|
||||||
|
|
||||||
|
|
||||||
authorize = extensions.extension_authorizer('share', 'types_manage')
|
|
||||||
|
|
||||||
|
|
||||||
class ShareTypesManageController(wsgi.Controller):
|
|
||||||
"""The share types API controller for the OpenStack API."""
|
|
||||||
|
|
||||||
_view_builder_class = views_types.ViewBuilder
|
|
||||||
|
|
||||||
def _notify_share_type_error(self, context, method, payload):
|
|
||||||
rpc.get_notifier('shareType').error(context, method, payload)
|
|
||||||
|
|
||||||
@wsgi.action("create")
|
|
||||||
def _create(self, req, body):
|
|
||||||
"""Creates a new share type."""
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
authorize(context)
|
|
||||||
|
|
||||||
if not self.is_valid_body(body, 'share_type') and \
|
|
||||||
not self.is_valid_body(body, 'volume_type'):
|
|
||||||
raise webob.exc.HTTPBadRequest()
|
|
||||||
|
|
||||||
elif self.is_valid_body(body, 'share_type'):
|
|
||||||
share_type = body['share_type']
|
|
||||||
else:
|
|
||||||
share_type = body['volume_type']
|
|
||||||
name = share_type.get('name', None)
|
|
||||||
specs = share_type.get('extra_specs', {})
|
|
||||||
is_public = share_type.get('os-share-type-access:is_public', True)
|
|
||||||
|
|
||||||
if name is None or name == "" or len(name) > 255:
|
|
||||||
msg = _("Type name is not valid.")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
required_extra_specs = (
|
|
||||||
share_types.get_valid_required_extra_specs(specs)
|
|
||||||
)
|
|
||||||
except exception.InvalidExtraSpec as e:
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=six.text_type(e))
|
|
||||||
|
|
||||||
try:
|
|
||||||
share_types.create(context, name, specs, is_public)
|
|
||||||
share_type = share_types.get_share_type_by_name(context, name)
|
|
||||||
share_type['required_extra_specs'] = required_extra_specs
|
|
||||||
req.cache_db_share_type(share_type)
|
|
||||||
notifier_info = dict(share_types=share_type)
|
|
||||||
rpc.get_notifier('shareType').info(
|
|
||||||
context, 'share_type.create', notifier_info)
|
|
||||||
|
|
||||||
except exception.ShareTypeExists as err:
|
|
||||||
notifier_err = dict(share_types=share_type,
|
|
||||||
error_message=six.text_type(err))
|
|
||||||
self._notify_share_type_error(context, 'share_type.create',
|
|
||||||
notifier_err)
|
|
||||||
|
|
||||||
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
|
||||||
except exception.NotFound as err:
|
|
||||||
notifier_err = dict(share_types=share_type,
|
|
||||||
error_message=six.text_type(err))
|
|
||||||
self._notify_share_type_error(context, 'share_type.create',
|
|
||||||
notifier_err)
|
|
||||||
raise webob.exc.HTTPNotFound()
|
|
||||||
|
|
||||||
return self._view_builder.show(req, share_type)
|
|
||||||
|
|
||||||
@wsgi.action("delete")
|
|
||||||
def _delete(self, req, id):
|
|
||||||
"""Deletes an existing share type."""
|
|
||||||
context = req.environ['manila.context']
|
|
||||||
authorize(context)
|
|
||||||
|
|
||||||
try:
|
|
||||||
share_type = share_types.get_share_type(context, id)
|
|
||||||
share_types.destroy(context, share_type['id'])
|
|
||||||
notifier_info = dict(share_types=share_type)
|
|
||||||
rpc.get_notifier('shareType').info(
|
|
||||||
context, 'share_type.delete', notifier_info)
|
|
||||||
except exception.ShareTypeInUse as err:
|
|
||||||
notifier_err = dict(id=id, error_message=six.text_type(err))
|
|
||||||
self._notify_share_type_error(context, 'share_type.delete',
|
|
||||||
notifier_err)
|
|
||||||
msg = 'Target share type is still in use.'
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
except exception.NotFound as err:
|
|
||||||
notifier_err = dict(id=id, error_message=six.text_type(err))
|
|
||||||
self._notify_share_type_error(context, 'share_type.delete',
|
|
||||||
notifier_err)
|
|
||||||
|
|
||||||
raise webob.exc.HTTPNotFound()
|
|
||||||
|
|
||||||
return webob.Response(status_int=202)
|
|
||||||
|
|
||||||
|
|
||||||
class Types_manage(extensions.ExtensionDescriptor):
|
|
||||||
"""Types manage support."""
|
|
||||||
|
|
||||||
name = "TypesManage"
|
|
||||||
alias = "os-types-manage"
|
|
||||||
updated = "2011-08-24T00:00:00+00:00"
|
|
||||||
|
|
||||||
def get_controller_extensions(self):
|
|
||||||
controller = ShareTypesManageController()
|
|
||||||
extension = extensions.ControllerExtension(self, 'types', controller)
|
|
||||||
return [extension]
|
|
@ -39,6 +39,7 @@ from manila.api.v1 import share_networks
|
|||||||
from manila.api.v1 import share_servers
|
from manila.api.v1 import share_servers
|
||||||
from manila.api.v1 import share_snapshots
|
from manila.api.v1 import share_snapshots
|
||||||
from manila.api.v1 import share_types
|
from manila.api.v1 import share_types
|
||||||
|
from manila.api.v1 import share_types_extra_specs
|
||||||
from manila.api.v1 import share_unmanage
|
from manila.api.v1 import share_unmanage
|
||||||
from manila.api.v1 import shares
|
from manila.api.v1 import shares
|
||||||
from manila.api import versions
|
from manila.api import versions
|
||||||
@ -176,7 +177,15 @@ class APIRouter(manila.api.openstack.APIRouter):
|
|||||||
mapper.resource("type", "types",
|
mapper.resource("type", "types",
|
||||||
controller=self.resources['types'],
|
controller=self.resources['types'],
|
||||||
collection={'detail': 'GET', 'default': 'GET'},
|
collection={'detail': 'GET', 'default': 'GET'},
|
||||||
member={'action': 'POST'})
|
member={'action': 'POST',
|
||||||
|
'os-share-type-access': 'GET'})
|
||||||
|
|
||||||
|
self.resources['extra_specs'] = (
|
||||||
|
share_types_extra_specs.create_resource())
|
||||||
|
mapper.resource('extra_spec', 'extra_specs',
|
||||||
|
controller=self.resources['extra_specs'],
|
||||||
|
parent_resource=dict(member_name='type',
|
||||||
|
collection_name='types'))
|
||||||
|
|
||||||
self.resources['scheduler_stats'] = scheduler_stats.create_resource()
|
self.resources['scheduler_stats'] = scheduler_stats.create_resource()
|
||||||
mapper.connect('pools', '/{project_id}/scheduler-stats/pools',
|
mapper.connect('pools', '/{project_id}/scheduler-stats/pools',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
# Copyright (c) 2011 OpenStack Foundation
|
||||||
# Copyright (c) 2014 NetApp, Inc.
|
# Copyright (c) 2014 NetApp, Inc.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -12,10 +13,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""The share type & share types extra specs extension."""
|
"""The share type API controller module.."""
|
||||||
|
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
import six
|
import six
|
||||||
|
import webob
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
from manila.api.openstack import wsgi
|
from manila.api.openstack import wsgi
|
||||||
@ -23,20 +26,38 @@ from manila.api.views import types as views_types
|
|||||||
from manila import exception
|
from manila import exception
|
||||||
from manila.i18n import _
|
from manila.i18n import _
|
||||||
from manila import policy
|
from manila import policy
|
||||||
|
from manila import rpc
|
||||||
from manila.share import share_types
|
from manila.share import share_types
|
||||||
|
|
||||||
RESOURCE_NAME = 'share_type'
|
|
||||||
|
|
||||||
|
|
||||||
class ShareTypesController(wsgi.Controller):
|
class ShareTypesController(wsgi.Controller):
|
||||||
"""The share types API controller for the OpenStack API."""
|
"""The share types API controller for the OpenStack API."""
|
||||||
|
|
||||||
|
resource_name = 'share_type'
|
||||||
_view_builder_class = views_types.ViewBuilder
|
_view_builder_class = views_types.ViewBuilder
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
if key == 'os-share-type-access':
|
||||||
|
return self._list_project_access
|
||||||
|
return super(self.__class__, self).__getattr__(key)
|
||||||
|
|
||||||
|
def _notify_share_type_error(self, context, method, payload):
|
||||||
|
rpc.get_notifier('shareType').error(context, method, payload)
|
||||||
|
|
||||||
|
def _check_body(self, body, action_name):
|
||||||
|
if not self.is_valid_body(body, action_name):
|
||||||
|
raise webob.exc.HTTPBadRequest()
|
||||||
|
access = body[action_name]
|
||||||
|
project = access.get('project')
|
||||||
|
if not uuidutils.is_uuid_like(project):
|
||||||
|
msg = _("Bad project format: "
|
||||||
|
"project is not in proper format (%s)") % project
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
def index(self, req):
|
def index(self, req):
|
||||||
"""Returns the list of share types."""
|
"""Returns the list of share types."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
policy.check_policy(context, RESOURCE_NAME, 'index')
|
policy.check_policy(context, self.resource_name, 'index')
|
||||||
|
|
||||||
limited_types = self._get_share_types(req)
|
limited_types = self._get_share_types(req)
|
||||||
req.cache_db_share_types(limited_types)
|
req.cache_db_share_types(limited_types)
|
||||||
@ -45,7 +66,7 @@ class ShareTypesController(wsgi.Controller):
|
|||||||
def show(self, req, id):
|
def show(self, req, id):
|
||||||
"""Return a single share type item."""
|
"""Return a single share type item."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
policy.check_policy(context, RESOURCE_NAME, 'show')
|
policy.check_policy(context, self.resource_name, 'show')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
share_type = share_types.get_share_type(context, id)
|
share_type = share_types.get_share_type(context, id)
|
||||||
@ -60,7 +81,7 @@ class ShareTypesController(wsgi.Controller):
|
|||||||
def default(self, req):
|
def default(self, req):
|
||||||
"""Return default volume type."""
|
"""Return default volume type."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
policy.check_policy(context, RESOURCE_NAME, 'default')
|
policy.check_policy(context, self.resource_name, 'default')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
share_type = share_types.get_default_share_type(context)
|
share_type = share_types.get_default_share_type(context)
|
||||||
@ -109,6 +130,148 @@ class ShareTypesController(wsgi.Controller):
|
|||||||
msg = _('Invalid is_public filter [%s]') % is_public
|
msg = _('Invalid is_public filter [%s]') % is_public
|
||||||
raise exc.HTTPBadRequest(explanation=msg)
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
@wsgi.action("create")
|
||||||
|
def _create(self, req, body):
|
||||||
|
"""Creates a new share type."""
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
self.authorize(context, 'create')
|
||||||
|
|
||||||
|
if not self.is_valid_body(body, 'share_type') and \
|
||||||
|
not self.is_valid_body(body, 'volume_type'):
|
||||||
|
raise webob.exc.HTTPBadRequest()
|
||||||
|
|
||||||
|
elif self.is_valid_body(body, 'share_type'):
|
||||||
|
share_type = body['share_type']
|
||||||
|
else:
|
||||||
|
share_type = body['volume_type']
|
||||||
|
name = share_type.get('name', None)
|
||||||
|
specs = share_type.get('extra_specs', {})
|
||||||
|
is_public = share_type.get('os-share-type-access:is_public', True)
|
||||||
|
|
||||||
|
if name is None or name == "" or len(name) > 255:
|
||||||
|
msg = _("Type name is not valid.")
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
required_extra_specs = (
|
||||||
|
share_types.get_valid_required_extra_specs(specs)
|
||||||
|
)
|
||||||
|
except exception.InvalidExtraSpec as e:
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=six.text_type(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_types.create(context, name, specs, is_public)
|
||||||
|
share_type = share_types.get_share_type_by_name(context, name)
|
||||||
|
share_type['required_extra_specs'] = required_extra_specs
|
||||||
|
req.cache_db_share_type(share_type)
|
||||||
|
notifier_info = dict(share_types=share_type)
|
||||||
|
rpc.get_notifier('shareType').info(
|
||||||
|
context, 'share_type.create', notifier_info)
|
||||||
|
|
||||||
|
except exception.ShareTypeExists as err:
|
||||||
|
notifier_err = dict(share_types=share_type,
|
||||||
|
error_message=six.text_type(err))
|
||||||
|
self._notify_share_type_error(context, 'share_type.create',
|
||||||
|
notifier_err)
|
||||||
|
|
||||||
|
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
||||||
|
except exception.NotFound as err:
|
||||||
|
notifier_err = dict(share_types=share_type,
|
||||||
|
error_message=six.text_type(err))
|
||||||
|
self._notify_share_type_error(context, 'share_type.create',
|
||||||
|
notifier_err)
|
||||||
|
raise webob.exc.HTTPNotFound()
|
||||||
|
|
||||||
|
return self._view_builder.show(req, share_type)
|
||||||
|
|
||||||
|
@wsgi.action("delete")
|
||||||
|
def _delete(self, req, id):
|
||||||
|
"""Deletes an existing share type."""
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
self.authorize(context, 'delete')
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_type = share_types.get_share_type(context, id)
|
||||||
|
share_types.destroy(context, share_type['id'])
|
||||||
|
notifier_info = dict(share_types=share_type)
|
||||||
|
rpc.get_notifier('shareType').info(
|
||||||
|
context, 'share_type.delete', notifier_info)
|
||||||
|
except exception.ShareTypeInUse as err:
|
||||||
|
notifier_err = dict(id=id, error_message=six.text_type(err))
|
||||||
|
self._notify_share_type_error(context, 'share_type.delete',
|
||||||
|
notifier_err)
|
||||||
|
msg = 'Target share type is still in use.'
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
except exception.NotFound as err:
|
||||||
|
notifier_err = dict(id=id, error_message=six.text_type(err))
|
||||||
|
self._notify_share_type_error(context, 'share_type.delete',
|
||||||
|
notifier_err)
|
||||||
|
|
||||||
|
raise webob.exc.HTTPNotFound()
|
||||||
|
|
||||||
|
return webob.Response(status_int=202)
|
||||||
|
|
||||||
|
def _list_project_access(self, req, id):
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
self.authorize(context, 'list_project_access')
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_type = share_types.get_share_type(
|
||||||
|
context, id, expected_fields=['projects'])
|
||||||
|
except exception.ShareTypeNotFound:
|
||||||
|
explanation = _("Share type %s not found.") % id
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=explanation)
|
||||||
|
|
||||||
|
if share_type['is_public']:
|
||||||
|
expl = _("Access list not available for public share types.")
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=expl)
|
||||||
|
|
||||||
|
# TODO(vponomaryov): move to views.
|
||||||
|
rval = []
|
||||||
|
for project_id in share_type['projects']:
|
||||||
|
rval.append(
|
||||||
|
{'share_type_id': share_type['id'], 'project_id': project_id}
|
||||||
|
)
|
||||||
|
return {'share_type_access': rval}
|
||||||
|
|
||||||
|
@wsgi.action('addProjectAccess')
|
||||||
|
def _add_project_access(self, req, id, body):
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
self.authorize(context, 'add_project_access')
|
||||||
|
self._check_body(body, 'addProjectAccess')
|
||||||
|
project = body['addProjectAccess']['project']
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_type = share_types.get_share_type(context, id)
|
||||||
|
|
||||||
|
if share_type['is_public']:
|
||||||
|
msg = _("Project cannot be added to public share_type.")
|
||||||
|
raise webob.exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
|
except exception.ShareTypeNotFound as err:
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_types.add_share_type_access(context, id, project)
|
||||||
|
except exception.ShareTypeAccessExists as err:
|
||||||
|
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
||||||
|
|
||||||
|
return webob.Response(status_int=202)
|
||||||
|
|
||||||
|
@wsgi.action('removeProjectAccess')
|
||||||
|
def _remove_project_access(self, req, id, body):
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
self.authorize(context, 'remove_project_access')
|
||||||
|
self._check_body(body, 'removeProjectAccess')
|
||||||
|
project = body['removeProjectAccess']['project']
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_types.remove_share_type_access(context, id, project)
|
||||||
|
except (exception.ShareTypeNotFound,
|
||||||
|
exception.ShareTypeAccessNotFound) as err:
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
||||||
|
return webob.Response(status_int=202)
|
||||||
|
|
||||||
|
|
||||||
def create_resource():
|
def create_resource():
|
||||||
return wsgi.Resource(ShareTypesController())
|
return wsgi.Resource(ShareTypesController())
|
||||||
|
@ -13,13 +13,10 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""The share types extra specs extension"""
|
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from manila.api import common
|
from manila.api import common
|
||||||
from manila.api import extensions
|
|
||||||
from manila.api.openstack import wsgi
|
from manila.api.openstack import wsgi
|
||||||
from manila import db
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
@ -27,12 +24,12 @@ from manila.i18n import _
|
|||||||
from manila import rpc
|
from manila import rpc
|
||||||
from manila.share import share_types
|
from manila.share import share_types
|
||||||
|
|
||||||
authorize = extensions.extension_authorizer('share', 'types_extra_specs')
|
|
||||||
|
|
||||||
|
|
||||||
class ShareTypeExtraSpecsController(wsgi.Controller):
|
class ShareTypeExtraSpecsController(wsgi.Controller):
|
||||||
"""The share type extra specs API controller for the OpenStack API."""
|
"""The share type extra specs API controller for the OpenStack API."""
|
||||||
|
|
||||||
|
resource_name = 'share_types_extra_spec'
|
||||||
|
|
||||||
def _get_extra_specs(self, context, type_id):
|
def _get_extra_specs(self, context, type_id):
|
||||||
extra_specs = db.share_type_extra_specs_get(context, type_id)
|
extra_specs = db.share_type_extra_specs_get(context, type_id)
|
||||||
specs_dict = {}
|
specs_dict = {}
|
||||||
@ -77,13 +74,13 @@ class ShareTypeExtraSpecsController(wsgi.Controller):
|
|||||||
def index(self, req, type_id):
|
def index(self, req, type_id):
|
||||||
"""Returns the list of extra specs for a given share type."""
|
"""Returns the list of extra specs for a given share type."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
authorize(context)
|
self.authorize(context, 'index')
|
||||||
self._check_type(context, type_id)
|
self._check_type(context, type_id)
|
||||||
return self._get_extra_specs(context, type_id)
|
return self._get_extra_specs(context, type_id)
|
||||||
|
|
||||||
def create(self, req, type_id, body=None):
|
def create(self, req, type_id, body=None):
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
authorize(context)
|
self.authorize(context, 'create')
|
||||||
|
|
||||||
if not self.is_valid_body(body, 'extra_specs'):
|
if not self.is_valid_body(body, 'extra_specs'):
|
||||||
raise webob.exc.HTTPBadRequest()
|
raise webob.exc.HTTPBadRequest()
|
||||||
@ -100,7 +97,7 @@ class ShareTypeExtraSpecsController(wsgi.Controller):
|
|||||||
|
|
||||||
def update(self, req, type_id, id, body=None):
|
def update(self, req, type_id, id, body=None):
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
authorize(context)
|
self.authorize(context, 'update')
|
||||||
if not body:
|
if not body:
|
||||||
expl = _('Request body empty')
|
expl = _('Request body empty')
|
||||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||||
@ -121,7 +118,7 @@ class ShareTypeExtraSpecsController(wsgi.Controller):
|
|||||||
def show(self, req, type_id, id):
|
def show(self, req, type_id, id):
|
||||||
"""Return a single extra spec item."""
|
"""Return a single extra spec item."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
authorize(context)
|
self.authorize(context, 'show')
|
||||||
self._check_type(context, type_id)
|
self._check_type(context, type_id)
|
||||||
specs = self._get_extra_specs(context, type_id)
|
specs = self._get_extra_specs(context, type_id)
|
||||||
if id in specs['extra_specs']:
|
if id in specs['extra_specs']:
|
||||||
@ -133,7 +130,7 @@ class ShareTypeExtraSpecsController(wsgi.Controller):
|
|||||||
"""Deletes an existing extra spec."""
|
"""Deletes an existing extra spec."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
self._check_type(context, type_id)
|
self._check_type(context, type_id)
|
||||||
authorize(context)
|
self.authorize(context, 'delete')
|
||||||
|
|
||||||
if id in share_types.get_undeletable_extra_specs():
|
if id in share_types.get_undeletable_extra_specs():
|
||||||
msg = _("Extra spec '%s' can't be deleted.") % id
|
msg = _("Extra spec '%s' can't be deleted.") % id
|
||||||
@ -157,21 +154,5 @@ class ShareTypeExtraSpecsController(wsgi.Controller):
|
|||||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||||
|
|
||||||
|
|
||||||
class Types_extra_specs(extensions.ExtensionDescriptor):
|
def create_resource():
|
||||||
"""Type extra specs support."""
|
return wsgi.Resource(ShareTypeExtraSpecsController())
|
||||||
|
|
||||||
name = "TypesExtraSpecs"
|
|
||||||
alias = "os-types-extra-specs"
|
|
||||||
updated = "2011-08-24T00:00:00+00:00"
|
|
||||||
|
|
||||||
def get_resources(self):
|
|
||||||
resources = []
|
|
||||||
res = extensions.ResourceExtension(
|
|
||||||
'extra_specs',
|
|
||||||
ShareTypeExtraSpecsController(),
|
|
||||||
parent=dict(member_name='type',
|
|
||||||
collection_name='types')
|
|
||||||
)
|
|
||||||
resources.append(res)
|
|
||||||
|
|
||||||
return resources
|
|
@ -35,10 +35,14 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
required_extra_specs = self._filter_extra_specs(
|
required_extra_specs = self._filter_extra_specs(
|
||||||
required_extra_specs, extra_spec_names)
|
required_extra_specs, extra_spec_names)
|
||||||
|
|
||||||
trimmed = dict(id=share_type.get('id'),
|
trimmed = {
|
||||||
name=share_type.get('name'),
|
'id': share_type.get('id'),
|
||||||
extra_specs=extra_specs,
|
'name': share_type.get('name'),
|
||||||
required_extra_specs=required_extra_specs)
|
'os-share-type-access:is_public': share_type.get(
|
||||||
|
'is_public', True),
|
||||||
|
'extra_specs': extra_specs,
|
||||||
|
'required_extra_specs': required_extra_specs,
|
||||||
|
}
|
||||||
if brief:
|
if brief:
|
||||||
return trimmed
|
return trimmed
|
||||||
else:
|
else:
|
||||||
|
@ -1,317 +0,0 @@
|
|||||||
#
|
|
||||||
# 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 datetime
|
|
||||||
|
|
||||||
import mock
|
|
||||||
import six
|
|
||||||
import webob
|
|
||||||
|
|
||||||
from manila.api.contrib import share_type_access as type_access
|
|
||||||
from manila.api.v1 import share_types
|
|
||||||
from manila import context
|
|
||||||
from manila import db
|
|
||||||
from manila import exception
|
|
||||||
from manila.share import share_types as share_types_api
|
|
||||||
from manila import test
|
|
||||||
from manila.tests.api import fakes
|
|
||||||
|
|
||||||
|
|
||||||
def generate_type(type_id, is_public):
|
|
||||||
return {
|
|
||||||
'id': type_id,
|
|
||||||
'name': u'test',
|
|
||||||
'deleted': False,
|
|
||||||
'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1),
|
|
||||||
'updated_at': None,
|
|
||||||
'deleted_at': None,
|
|
||||||
'is_public': bool(is_public),
|
|
||||||
'extra_specs': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SHARE_TYPES = {
|
|
||||||
'0': generate_type('0', True),
|
|
||||||
'1': generate_type('1', True),
|
|
||||||
'2': generate_type('2', False),
|
|
||||||
'3': generate_type('3', False)}
|
|
||||||
|
|
||||||
PROJ1_UUID = '11111111-1111-1111-1111-111111111111'
|
|
||||||
PROJ2_UUID = '22222222-2222-2222-2222-222222222222'
|
|
||||||
PROJ3_UUID = '33333333-3333-3333-3333-333333333333'
|
|
||||||
|
|
||||||
ACCESS_LIST = [{'share_type_id': '2', 'project_id': PROJ2_UUID},
|
|
||||||
{'share_type_id': '2', 'project_id': PROJ3_UUID},
|
|
||||||
{'share_type_id': '3', 'project_id': PROJ3_UUID}]
|
|
||||||
|
|
||||||
|
|
||||||
def fake_share_type_get(context, id, inactive=False, expected_fields=None):
|
|
||||||
vol = SHARE_TYPES[id]
|
|
||||||
if expected_fields and 'projects' in expected_fields:
|
|
||||||
vol['projects'] = [a['project_id']
|
|
||||||
for a in ACCESS_LIST if a['share_type_id'] == id]
|
|
||||||
return vol
|
|
||||||
|
|
||||||
|
|
||||||
def _has_type_access(type_id, project_id):
|
|
||||||
for access in ACCESS_LIST:
|
|
||||||
if (access['share_type_id'] == type_id
|
|
||||||
and access['project_id'] == project_id):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def fake_share_type_get_all(context, inactive=False, filters=None):
|
|
||||||
if filters is None or filters.get('is_public', None) is None:
|
|
||||||
return SHARE_TYPES
|
|
||||||
res = {}
|
|
||||||
for k, v in six.iteritems(SHARE_TYPES):
|
|
||||||
if filters['is_public'] and _has_type_access(k, context.project_id):
|
|
||||||
res.update({k: v})
|
|
||||||
continue
|
|
||||||
if v['is_public'] == filters['is_public']:
|
|
||||||
res.update({k: v})
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class FakeResponse(object):
|
|
||||||
obj = {'share_type': {'id': '0'},
|
|
||||||
'share_types': [
|
|
||||||
{'id': '0'},
|
|
||||||
{'id': '2'}]}
|
|
||||||
|
|
||||||
def attach(self, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FakeRequest(object):
|
|
||||||
environ = {"manila.context": context.get_admin_context()}
|
|
||||||
|
|
||||||
def get_db_share_type(self, resource_id):
|
|
||||||
return SHARE_TYPES[resource_id]
|
|
||||||
|
|
||||||
|
|
||||||
class ShareTypeAccessTest(test.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(ShareTypeAccessTest, self).setUp()
|
|
||||||
self.type_access_controller = type_access.ShareTypeAccessController()
|
|
||||||
self.type_action_controller = type_access.ShareTypeActionController()
|
|
||||||
self.type_controller = share_types.ShareTypesController()
|
|
||||||
self.req = FakeRequest()
|
|
||||||
self.context = self.req.environ['manila.context']
|
|
||||||
self.mock_object(db, 'share_type_get',
|
|
||||||
fake_share_type_get)
|
|
||||||
self.mock_object(db, 'share_type_get_all',
|
|
||||||
fake_share_type_get_all)
|
|
||||||
|
|
||||||
def assertShareTypeListEqual(self, expected, observed):
|
|
||||||
self.assertEqual(len(expected), len(observed))
|
|
||||||
expected = sorted(expected, key=lambda item: item['id'])
|
|
||||||
observed = sorted(observed, key=lambda item: item['id'])
|
|
||||||
for d1, d2 in zip(expected, observed):
|
|
||||||
self.assertEqual(d1['id'], d2['id'])
|
|
||||||
|
|
||||||
def test_list_type_access_public(self):
|
|
||||||
"""Querying os-share-type-access on public type should return 404."""
|
|
||||||
req = fakes.HTTPRequest.blank('/v1/fake/types/os-share-type-access',
|
|
||||||
use_admin_context=True)
|
|
||||||
self.assertRaises(webob.exc.HTTPNotFound,
|
|
||||||
self.type_access_controller.index,
|
|
||||||
req, '1')
|
|
||||||
|
|
||||||
def test_list_type_access_private(self):
|
|
||||||
expected = {'share_type_access': [
|
|
||||||
{'share_type_id': '2', 'project_id': PROJ2_UUID},
|
|
||||||
{'share_type_id': '2', 'project_id': PROJ3_UUID}]}
|
|
||||||
result = self.type_access_controller.index(self.req, '2')
|
|
||||||
self.assertEqual(expected, result)
|
|
||||||
|
|
||||||
def test_list_with_no_context(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/v1/types/fake/types')
|
|
||||||
|
|
||||||
def fake_authorize(context, target=None, action=None):
|
|
||||||
raise exception.PolicyNotAuthorized(action='index')
|
|
||||||
self.mock_object(type_access, 'authorize', fake_authorize)
|
|
||||||
|
|
||||||
self.assertRaises(exception.PolicyNotAuthorized,
|
|
||||||
self.type_access_controller.index,
|
|
||||||
req, 'fake')
|
|
||||||
|
|
||||||
def test_list_type_with_admin_default_proj1(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v1/fake/types',
|
|
||||||
use_admin_context=True)
|
|
||||||
req.environ['manila.context'].project_id = PROJ1_UUID
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_admin_default_proj2(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}, {'id': '2'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
|
||||||
use_admin_context=True)
|
|
||||||
req.environ['manila.context'].project_id = PROJ2_UUID
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_admin_ispublic_true(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
|
||||||
use_admin_context=True)
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_admin_ispublic_false(self):
|
|
||||||
expected = {'share_types': [{'id': '2'}, {'id': '3'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
|
||||||
use_admin_context=True)
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_admin_ispublic_false_proj2(self):
|
|
||||||
expected = {'share_types': [{'id': '2'}, {'id': '3'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
|
||||||
use_admin_context=True)
|
|
||||||
req.environ['manila.context'].project_id = PROJ2_UUID
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_admin_ispublic_none(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}, {'id': '2'},
|
|
||||||
{'id': '3'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=all',
|
|
||||||
use_admin_context=True)
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_no_admin_default(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
|
||||||
use_admin_context=False)
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_no_admin_ispublic_true(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
|
||||||
use_admin_context=False)
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_no_admin_ispublic_false(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
|
||||||
use_admin_context=False)
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_list_type_with_no_admin_ispublic_none(self):
|
|
||||||
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=all',
|
|
||||||
use_admin_context=False)
|
|
||||||
result = self.type_controller.index(req)
|
|
||||||
self.assertShareTypeListEqual(expected['share_types'],
|
|
||||||
result['share_types'])
|
|
||||||
|
|
||||||
def test_show(self):
|
|
||||||
resp = FakeResponse()
|
|
||||||
self.type_action_controller.show(self.req, resp, '0')
|
|
||||||
self.assertEqual({'id': '0', 'os-share-type-access:is_public': True},
|
|
||||||
resp.obj['share_type'])
|
|
||||||
self.type_action_controller.show(self.req, resp, '2')
|
|
||||||
self.assertEqual({'id': '0', 'os-share-type-access:is_public': False},
|
|
||||||
resp.obj['share_type'])
|
|
||||||
|
|
||||||
def test_create(self):
|
|
||||||
resp = FakeResponse()
|
|
||||||
self.type_action_controller.create(self.req, {}, resp)
|
|
||||||
self.assertEqual({'id': '0', 'os-share-type-access:is_public': True},
|
|
||||||
resp.obj['share_type'])
|
|
||||||
|
|
||||||
def test_add_project_access(self):
|
|
||||||
def stub_add_share_type_access(context, type_id, project_id):
|
|
||||||
self.assertEqual('3', type_id, "type_id")
|
|
||||||
self.assertEqual(PROJ2_UUID, project_id, "project_id")
|
|
||||||
self.mock_object(db, 'share_type_access_add',
|
|
||||||
stub_add_share_type_access)
|
|
||||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
|
||||||
use_admin_context=True)
|
|
||||||
result = self.type_action_controller._addProjectAccess(req, '3', body)
|
|
||||||
self.assertEqual(202, result.status_code)
|
|
||||||
|
|
||||||
def test_add_project_access_with_no_admin_user(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
|
||||||
use_admin_context=False)
|
|
||||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
|
||||||
self.assertRaises(exception.PolicyNotAuthorized,
|
|
||||||
self.type_action_controller._addProjectAccess,
|
|
||||||
req, '2', body)
|
|
||||||
|
|
||||||
def test_add_project_access_with_already_added_access(self):
|
|
||||||
def stub_add_share_type_access(context, type_id, project_id):
|
|
||||||
raise exception.ShareTypeAccessExists(share_type_id=type_id,
|
|
||||||
project_id=project_id)
|
|
||||||
self.mock_object(db, 'share_type_access_add',
|
|
||||||
stub_add_share_type_access)
|
|
||||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
|
||||||
use_admin_context=True)
|
|
||||||
self.assertRaises(webob.exc.HTTPConflict,
|
|
||||||
self.type_action_controller._addProjectAccess,
|
|
||||||
req, '3', body)
|
|
||||||
|
|
||||||
def test_add_project_access_to_public_share_type(self):
|
|
||||||
share_type_id = '3'
|
|
||||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
|
||||||
self.mock_object(share_types_api, 'get_share_type',
|
|
||||||
mock.Mock(return_value={"is_public": True}))
|
|
||||||
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
|
||||||
use_admin_context=True)
|
|
||||||
|
|
||||||
self.assertRaises(webob.exc.HTTPForbidden,
|
|
||||||
self.type_action_controller._addProjectAccess,
|
|
||||||
req, share_type_id, body)
|
|
||||||
share_types_api.get_share_type.assert_called_once_with(
|
|
||||||
mock.ANY, share_type_id)
|
|
||||||
|
|
||||||
def test_remove_project_access_with_bad_access(self):
|
|
||||||
def stub_remove_share_type_access(context, type_id, project_id):
|
|
||||||
raise exception.ShareTypeAccessNotFound(share_type_id=type_id,
|
|
||||||
project_id=project_id)
|
|
||||||
self.mock_object(db, 'share_type_access_remove',
|
|
||||||
stub_remove_share_type_access)
|
|
||||||
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
|
||||||
use_admin_context=True)
|
|
||||||
self.assertRaises(webob.exc.HTTPNotFound,
|
|
||||||
self.type_action_controller._removeProjectAccess,
|
|
||||||
req, '3', body)
|
|
||||||
|
|
||||||
def test_remove_project_access_with_no_admin_user(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
|
||||||
use_admin_context=False)
|
|
||||||
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
|
||||||
self.assertRaises(exception.PolicyNotAuthorized,
|
|
||||||
self.type_action_controller._removeProjectAccess,
|
|
||||||
req, '2', body)
|
|
@ -1,153 +0,0 @@
|
|||||||
# Copyright 2011 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 ddt
|
|
||||||
import webob
|
|
||||||
|
|
||||||
from manila.api.contrib import types_manage
|
|
||||||
from manila.common import constants
|
|
||||||
from manila import exception
|
|
||||||
from manila.share import share_types
|
|
||||||
from manila import test
|
|
||||||
from manila.tests.api import fakes
|
|
||||||
from manila.tests import fake_notifier
|
|
||||||
|
|
||||||
|
|
||||||
def stub_share_type(id):
|
|
||||||
specs = {"key1": "value1",
|
|
||||||
"key2": "value2",
|
|
||||||
"key3": "value3",
|
|
||||||
"key4": "value4",
|
|
||||||
"key5": "value5"}
|
|
||||||
return dict(id=id, name='share_type_%s' % str(id), extra_specs=specs)
|
|
||||||
|
|
||||||
|
|
||||||
def return_share_types_get_share_type(context, id):
|
|
||||||
if id == "777":
|
|
||||||
raise exception.ShareTypeNotFound(share_type_id=id)
|
|
||||||
return stub_share_type(int(id))
|
|
||||||
|
|
||||||
|
|
||||||
def return_share_types_destroy(context, name):
|
|
||||||
if name == "777":
|
|
||||||
raise exception.ShareTypeNotFoundByName(share_type_name=name)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def return_share_types_with_volumes_destroy(context, id):
|
|
||||||
if id == "1":
|
|
||||||
raise exception.ShareTypeInUse(share_type_id=id)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def return_share_types_create(context, name, specs, is_public):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def return_share_types_get_by_name(context, name):
|
|
||||||
if name == "777":
|
|
||||||
raise exception.ShareTypeNotFoundByName(share_type_name=name)
|
|
||||||
return stub_share_type(int(name.split("_")[2]))
|
|
||||||
|
|
||||||
|
|
||||||
def make_create_body(name="test_share_1", extra_specs=None,
|
|
||||||
spec_driver_handles_share_servers=True):
|
|
||||||
if not extra_specs:
|
|
||||||
extra_specs = {}
|
|
||||||
|
|
||||||
if spec_driver_handles_share_servers is not None:
|
|
||||||
extra_specs[constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS] =\
|
|
||||||
spec_driver_handles_share_servers
|
|
||||||
|
|
||||||
body = {
|
|
||||||
"share_type": {
|
|
||||||
"name": name,
|
|
||||||
"extra_specs": extra_specs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
|
||||||
class ShareTypesManageApiTest(test.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(ShareTypesManageApiTest, self).setUp()
|
|
||||||
self.flags(host='fake')
|
|
||||||
self.controller = types_manage.ShareTypesManageController()
|
|
||||||
|
|
||||||
"""to reset notifier drivers left over from other api/contrib tests"""
|
|
||||||
fake_notifier.reset()
|
|
||||||
self.addCleanup(fake_notifier.reset)
|
|
||||||
self.mock_object(share_types, 'create',
|
|
||||||
return_share_types_create)
|
|
||||||
self.mock_object(share_types, 'get_share_type_by_name',
|
|
||||||
return_share_types_get_by_name)
|
|
||||||
self.mock_object(share_types, 'get_share_type',
|
|
||||||
return_share_types_get_share_type)
|
|
||||||
self.mock_object(share_types, 'destroy',
|
|
||||||
return_share_types_destroy)
|
|
||||||
|
|
||||||
def test_share_types_delete(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
|
||||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
self.controller._delete(req, 1)
|
|
||||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
|
|
||||||
def test_share_types_delete_not_found(self):
|
|
||||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/777')
|
|
||||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete,
|
|
||||||
req, '777')
|
|
||||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
|
|
||||||
def test_share_types_with_volumes_destroy(self):
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
|
||||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
self.controller._delete(req, 1)
|
|
||||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
|
|
||||||
@ddt.data(make_create_body("share_type_1"),
|
|
||||||
make_create_body(spec_driver_handles_share_servers="false"),
|
|
||||||
make_create_body(spec_driver_handles_share_servers="true"),
|
|
||||||
make_create_body(spec_driver_handles_share_servers="1"),
|
|
||||||
make_create_body(spec_driver_handles_share_servers="0"),
|
|
||||||
make_create_body(spec_driver_handles_share_servers="True"),
|
|
||||||
make_create_body(spec_driver_handles_share_servers="False"),
|
|
||||||
make_create_body(spec_driver_handles_share_servers="FalsE"))
|
|
||||||
def test_create(self, body):
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types')
|
|
||||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
res_dict = self.controller._create(req, body)
|
|
||||||
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
self.assertEqual(2, len(res_dict))
|
|
||||||
self.assertEqual('share_type_1', res_dict['share_type']['name'])
|
|
||||||
self.assertEqual('share_type_1', res_dict['volume_type']['name'])
|
|
||||||
|
|
||||||
@ddt.data(None,
|
|
||||||
make_create_body(""),
|
|
||||||
make_create_body("n" * 256),
|
|
||||||
{'foo': {'a': 'b'}},
|
|
||||||
{'share_type': 'string'},
|
|
||||||
make_create_body(spec_driver_handles_share_servers=None),
|
|
||||||
make_create_body(spec_driver_handles_share_servers=""),
|
|
||||||
make_create_body(spec_driver_handles_share_servers=[]),
|
|
||||||
)
|
|
||||||
def test_create_invalid_request(self, body):
|
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/types')
|
|
||||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
|
||||||
self.controller._create, req, body)
|
|
||||||
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
|
@ -13,19 +13,25 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
import mock
|
import mock
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
import six
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from manila.api.v1 import share_types as types
|
from manila.api.v1 import share_types as types
|
||||||
from manila.api.views import types as views_types
|
from manila.api.views import types as views_types
|
||||||
from manila.common import constants
|
from manila.common import constants
|
||||||
|
from manila import context
|
||||||
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila import policy
|
from manila import policy
|
||||||
from manila.share import share_types
|
from manila.share import share_types
|
||||||
from manila import test
|
from manila import test
|
||||||
from manila.tests.api import fakes
|
from manila.tests.api import fakes
|
||||||
|
from manila.tests import fake_notifier
|
||||||
|
|
||||||
|
|
||||||
def stub_share_type(id):
|
def stub_share_type(id):
|
||||||
@ -71,13 +77,60 @@ def return_share_types_get_by_name(context, name):
|
|||||||
return stub_share_type(int(name.split("_")[2]))
|
return stub_share_type(int(name.split("_")[2]))
|
||||||
|
|
||||||
|
|
||||||
|
def return_share_types_destroy(context, name):
|
||||||
|
if name == "777":
|
||||||
|
raise exception.ShareTypeNotFoundByName(share_type_name=name)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def return_share_types_with_volumes_destroy(context, id):
|
||||||
|
if id == "1":
|
||||||
|
raise exception.ShareTypeInUse(share_type_id=id)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def return_share_types_create(context, name, specs, is_public):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def make_create_body(name="test_share_1", extra_specs=None,
|
||||||
|
spec_driver_handles_share_servers=True):
|
||||||
|
if not extra_specs:
|
||||||
|
extra_specs = {}
|
||||||
|
|
||||||
|
if spec_driver_handles_share_servers is not None:
|
||||||
|
extra_specs[constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS] =\
|
||||||
|
spec_driver_handles_share_servers
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"share_type": {
|
||||||
|
"name": name,
|
||||||
|
"extra_specs": extra_specs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class ShareTypesApiTest(test.TestCase):
|
class ShareTypesAPITest(test.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ShareTypesApiTest, self).setUp()
|
super(self.__class__, self).setUp()
|
||||||
|
self.flags(host='fake')
|
||||||
self.controller = types.ShareTypesController()
|
self.controller = types.ShareTypesController()
|
||||||
self.mock_object(policy, 'check_policy',
|
self.mock_object(policy, 'check_policy',
|
||||||
mock.Mock(return_value=True))
|
mock.Mock(return_value=True))
|
||||||
|
fake_notifier.reset()
|
||||||
|
self.addCleanup(fake_notifier.reset)
|
||||||
|
self.mock_object(share_types, 'create',
|
||||||
|
return_share_types_create)
|
||||||
|
self.mock_object(share_types, 'get_share_type_by_name',
|
||||||
|
return_share_types_get_by_name)
|
||||||
|
self.mock_object(share_types, 'get_share_type',
|
||||||
|
return_share_types_get_share_type)
|
||||||
|
self.mock_object(share_types, 'destroy',
|
||||||
|
return_share_types_destroy)
|
||||||
|
|
||||||
@ddt.data(True, False)
|
@ddt.data(True, False)
|
||||||
def test_share_types_index(self, admin):
|
def test_share_types_index(self, admin):
|
||||||
@ -104,7 +157,8 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS, '')
|
constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS, '')
|
||||||
self.assertEqual('true', required_extra_spec)
|
self.assertEqual('true', required_extra_spec)
|
||||||
policy.check_policy.assert_called_once_with(
|
policy.check_policy.assert_called_once_with(
|
||||||
req.environ['manila.context'], types.RESOURCE_NAME, 'index')
|
req.environ['manila.context'], self.controller.resource_name,
|
||||||
|
'index')
|
||||||
|
|
||||||
def test_share_types_index_no_data(self):
|
def test_share_types_index_no_data(self):
|
||||||
self.mock_object(share_types, 'get_all_types',
|
self.mock_object(share_types, 'get_all_types',
|
||||||
@ -115,7 +169,8 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(0, len(res_dict['share_types']))
|
self.assertEqual(0, len(res_dict['share_types']))
|
||||||
policy.check_policy.assert_called_once_with(
|
policy.check_policy.assert_called_once_with(
|
||||||
req.environ['manila.context'], types.RESOURCE_NAME, 'index')
|
req.environ['manila.context'], self.controller.resource_name,
|
||||||
|
'index')
|
||||||
|
|
||||||
def test_share_types_show(self):
|
def test_share_types_show(self):
|
||||||
self.mock_object(share_types, 'get_share_type',
|
self.mock_object(share_types, 'get_share_type',
|
||||||
@ -128,7 +183,8 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
self.assertEqual('1', res_dict['share_type']['id'])
|
self.assertEqual('1', res_dict['share_type']['id'])
|
||||||
self.assertEqual('share_type_1', res_dict['share_type']['name'])
|
self.assertEqual('share_type_1', res_dict['share_type']['name'])
|
||||||
policy.check_policy.assert_called_once_with(
|
policy.check_policy.assert_called_once_with(
|
||||||
req.environ['manila.context'], types.RESOURCE_NAME, 'show')
|
req.environ['manila.context'], self.controller.resource_name,
|
||||||
|
'show')
|
||||||
|
|
||||||
def test_share_types_show_not_found(self):
|
def test_share_types_show_not_found(self):
|
||||||
self.mock_object(share_types, 'get_share_type',
|
self.mock_object(share_types, 'get_share_type',
|
||||||
@ -138,7 +194,8 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
||||||
req, '777')
|
req, '777')
|
||||||
policy.check_policy.assert_called_once_with(
|
policy.check_policy.assert_called_once_with(
|
||||||
req.environ['manila.context'], types.RESOURCE_NAME, 'show')
|
req.environ['manila.context'], self.controller.resource_name,
|
||||||
|
'show')
|
||||||
|
|
||||||
def test_share_types_default(self):
|
def test_share_types_default(self):
|
||||||
self.mock_object(share_types, 'get_default_share_type',
|
self.mock_object(share_types, 'get_default_share_type',
|
||||||
@ -151,7 +208,8 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
self.assertEqual('1', res_dict['share_type']['id'])
|
self.assertEqual('1', res_dict['share_type']['id'])
|
||||||
self.assertEqual('share_type_1', res_dict['share_type']['name'])
|
self.assertEqual('share_type_1', res_dict['share_type']['name'])
|
||||||
policy.check_policy.assert_called_once_with(
|
policy.check_policy.assert_called_once_with(
|
||||||
req.environ['manila.context'], types.RESOURCE_NAME, 'default')
|
req.environ['manila.context'], self.controller.resource_name,
|
||||||
|
'default')
|
||||||
|
|
||||||
def test_share_types_default_not_found(self):
|
def test_share_types_default_not_found(self):
|
||||||
self.mock_object(share_types, 'get_default_share_type',
|
self.mock_object(share_types, 'get_default_share_type',
|
||||||
@ -161,7 +219,8 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.default, req)
|
self.assertRaises(webob.exc.HTTPNotFound, self.controller.default, req)
|
||||||
policy.check_policy.assert_called_once_with(
|
policy.check_policy.assert_called_once_with(
|
||||||
req.environ['manila.context'], types.RESOURCE_NAME, 'default')
|
req.environ['manila.context'], self.controller.resource_name,
|
||||||
|
'default')
|
||||||
|
|
||||||
def test_view_builder_show(self):
|
def test_view_builder_show(self):
|
||||||
view_builder = views_types.ViewBuilder()
|
view_builder = views_types.ViewBuilder()
|
||||||
@ -182,12 +241,13 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
output = view_builder.show(request, raw_share_type)
|
output = view_builder.show(request, raw_share_type)
|
||||||
|
|
||||||
self.assertIn('share_type', output)
|
self.assertIn('share_type', output)
|
||||||
expected_share_type = dict(
|
expected_share_type = {
|
||||||
name='new_type',
|
'name': 'new_type',
|
||||||
extra_specs={},
|
'extra_specs': {},
|
||||||
required_extra_specs={},
|
'os-share-type-access:is_public': True,
|
||||||
id=42,
|
'required_extra_specs': {},
|
||||||
)
|
'id': 42,
|
||||||
|
}
|
||||||
self.assertDictMatch(output['share_type'], expected_share_type)
|
self.assertDictMatch(output['share_type'], expected_share_type)
|
||||||
|
|
||||||
def test_view_builder_list(self):
|
def test_view_builder_list(self):
|
||||||
@ -214,12 +274,13 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
|
|
||||||
self.assertIn('share_types', output)
|
self.assertIn('share_types', output)
|
||||||
for i in range(0, 10):
|
for i in range(0, 10):
|
||||||
expected_share_type = dict(
|
expected_share_type = {
|
||||||
name='new_type',
|
'name': 'new_type',
|
||||||
extra_specs={},
|
'extra_specs': {},
|
||||||
required_extra_specs={},
|
'os-share-type-access:is_public': True,
|
||||||
id=42 + i
|
'required_extra_specs': {},
|
||||||
)
|
'id': 42 + i,
|
||||||
|
}
|
||||||
self.assertDictMatch(output['share_types'][i],
|
self.assertDictMatch(output['share_types'][i],
|
||||||
expected_share_type)
|
expected_share_type)
|
||||||
|
|
||||||
@ -232,3 +293,359 @@ class ShareTypesApiTest(test.TestCase):
|
|||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
self.controller._parse_is_public,
|
self.controller._parse_is_public,
|
||||||
'fakefakefake')
|
'fakefakefake')
|
||||||
|
|
||||||
|
def test_share_types_delete(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
||||||
|
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
self.controller._delete(req, 1)
|
||||||
|
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
|
||||||
|
def test_share_types_delete_not_found(self):
|
||||||
|
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/777')
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete,
|
||||||
|
req, '777')
|
||||||
|
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
|
||||||
|
def test_share_types_with_volumes_destroy(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
|
||||||
|
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
self.controller._delete(req, 1)
|
||||||
|
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
|
||||||
|
@ddt.data(make_create_body("share_type_1"),
|
||||||
|
make_create_body(spec_driver_handles_share_servers="false"),
|
||||||
|
make_create_body(spec_driver_handles_share_servers="true"),
|
||||||
|
make_create_body(spec_driver_handles_share_servers="1"),
|
||||||
|
make_create_body(spec_driver_handles_share_servers="0"),
|
||||||
|
make_create_body(spec_driver_handles_share_servers="True"),
|
||||||
|
make_create_body(spec_driver_handles_share_servers="False"),
|
||||||
|
make_create_body(spec_driver_handles_share_servers="FalsE"))
|
||||||
|
def test_create(self, body):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types')
|
||||||
|
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
res_dict = self.controller._create(req, body)
|
||||||
|
self.assertEqual(1, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
self.assertEqual(2, len(res_dict))
|
||||||
|
self.assertEqual('share_type_1', res_dict['share_type']['name'])
|
||||||
|
self.assertEqual('share_type_1', res_dict['volume_type']['name'])
|
||||||
|
|
||||||
|
@ddt.data(None,
|
||||||
|
make_create_body(""),
|
||||||
|
make_create_body("n" * 256),
|
||||||
|
{'foo': {'a': 'b'}},
|
||||||
|
{'share_type': 'string'},
|
||||||
|
make_create_body(spec_driver_handles_share_servers=None),
|
||||||
|
make_create_body(spec_driver_handles_share_servers=""),
|
||||||
|
make_create_body(spec_driver_handles_share_servers=[]),
|
||||||
|
)
|
||||||
|
def test_create_invalid_request(self, body):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types')
|
||||||
|
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._create, req, body)
|
||||||
|
self.assertEqual(0, len(fake_notifier.NOTIFICATIONS))
|
||||||
|
|
||||||
|
def assert_share_type_list_equal(self, expected, observed):
|
||||||
|
self.assertEqual(len(expected), len(observed))
|
||||||
|
expected = sorted(expected, key=lambda item: item['id'])
|
||||||
|
observed = sorted(observed, key=lambda item: item['id'])
|
||||||
|
for d1, d2 in zip(expected, observed):
|
||||||
|
self.assertEqual(d1['id'], d2['id'])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_type(type_id, is_public):
|
||||||
|
return {
|
||||||
|
'id': type_id,
|
||||||
|
'name': u'test',
|
||||||
|
'deleted': False,
|
||||||
|
'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1),
|
||||||
|
'updated_at': None,
|
||||||
|
'deleted_at': None,
|
||||||
|
'is_public': bool(is_public),
|
||||||
|
'extra_specs': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SHARE_TYPES = {
|
||||||
|
'0': generate_type('0', True),
|
||||||
|
'1': generate_type('1', True),
|
||||||
|
'2': generate_type('2', False),
|
||||||
|
'3': generate_type('3', False)}
|
||||||
|
|
||||||
|
PROJ1_UUID = '11111111-1111-1111-1111-111111111111'
|
||||||
|
PROJ2_UUID = '22222222-2222-2222-2222-222222222222'
|
||||||
|
PROJ3_UUID = '33333333-3333-3333-3333-333333333333'
|
||||||
|
|
||||||
|
ACCESS_LIST = [{'share_type_id': '2', 'project_id': PROJ2_UUID},
|
||||||
|
{'share_type_id': '2', 'project_id': PROJ3_UUID},
|
||||||
|
{'share_type_id': '3', 'project_id': PROJ3_UUID}]
|
||||||
|
|
||||||
|
|
||||||
|
def fake_share_type_get(context, id, inactive=False, expected_fields=None):
|
||||||
|
vol = SHARE_TYPES[id]
|
||||||
|
if expected_fields and 'projects' in expected_fields:
|
||||||
|
vol['projects'] = [a['project_id']
|
||||||
|
for a in ACCESS_LIST if a['share_type_id'] == id]
|
||||||
|
return vol
|
||||||
|
|
||||||
|
|
||||||
|
def _has_type_access(type_id, project_id):
|
||||||
|
for access in ACCESS_LIST:
|
||||||
|
if (access['share_type_id'] == type_id
|
||||||
|
and access['project_id'] == project_id):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def fake_share_type_get_all(context, inactive=False, filters=None):
|
||||||
|
if filters is None or filters.get('is_public', None) is None:
|
||||||
|
return SHARE_TYPES
|
||||||
|
res = {}
|
||||||
|
for k, v in six.iteritems(SHARE_TYPES):
|
||||||
|
if filters['is_public'] and _has_type_access(k, context.project_id):
|
||||||
|
res.update({k: v})
|
||||||
|
continue
|
||||||
|
if v['is_public'] == filters['is_public']:
|
||||||
|
res.update({k: v})
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResponse(object):
|
||||||
|
obj = {'share_type': {'id': '0'},
|
||||||
|
'share_types': [{'id': '0'}, {'id': '2'}]}
|
||||||
|
|
||||||
|
def attach(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRequest(object):
|
||||||
|
environ = {"manila.context": context.get_admin_context()}
|
||||||
|
|
||||||
|
def get_db_share_type(self, resource_id):
|
||||||
|
return SHARE_TYPES[resource_id]
|
||||||
|
|
||||||
|
|
||||||
|
class ShareTypeAccessTest(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(self.__class__, self).setUp()
|
||||||
|
self.controller = types.ShareTypesController()
|
||||||
|
self.req = FakeRequest()
|
||||||
|
self.context = self.req.environ['manila.context']
|
||||||
|
self.mock_object(db, 'share_type_get', fake_share_type_get)
|
||||||
|
self.mock_object(db, 'share_type_get_all', fake_share_type_get_all)
|
||||||
|
|
||||||
|
def assertShareTypeListEqual(self, expected, observed):
|
||||||
|
self.assertEqual(len(expected), len(observed))
|
||||||
|
expected = sorted(expected, key=lambda item: item['id'])
|
||||||
|
observed = sorted(observed, key=lambda item: item['id'])
|
||||||
|
for d1, d2 in zip(expected, observed):
|
||||||
|
self.assertEqual(d1['id'], d2['id'])
|
||||||
|
|
||||||
|
def test_list_type_access_public(self):
|
||||||
|
"""Querying os-share-type-access on public type should return 404."""
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/fake/types/os-share-type-access',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller._list_project_access,
|
||||||
|
req, '1')
|
||||||
|
|
||||||
|
def test_list_type_access_private(self):
|
||||||
|
expected = {'share_type_access': [
|
||||||
|
{'share_type_id': '2', 'project_id': PROJ2_UUID},
|
||||||
|
{'share_type_id': '2', 'project_id': PROJ3_UUID},
|
||||||
|
]}
|
||||||
|
|
||||||
|
result = self.controller._list_project_access(self.req, '2')
|
||||||
|
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_list_with_no_context(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/types/fake/types')
|
||||||
|
|
||||||
|
def fake_authorize(context, target=None, action=None):
|
||||||
|
raise exception.PolicyNotAuthorized(action='index')
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller._list_project_access,
|
||||||
|
req, 'fake')
|
||||||
|
|
||||||
|
def test_list_type_with_admin_default_proj1(self):
|
||||||
|
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v1/fake/types', use_admin_context=True)
|
||||||
|
req.environ['manila.context'].project_id = PROJ1_UUID
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_admin_default_proj2(self):
|
||||||
|
expected = {'share_types': [{'id': '0'}, {'id': '1'}, {'id': '2'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types', use_admin_context=True)
|
||||||
|
req.environ['manila.context'].project_id = PROJ2_UUID
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_admin_ispublic_true(self):
|
||||||
|
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_admin_ispublic_false(self):
|
||||||
|
expected = {'share_types': [{'id': '2'}, {'id': '3'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_admin_ispublic_false_proj2(self):
|
||||||
|
expected = {'share_types': [{'id': '2'}, {'id': '3'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||||
|
use_admin_context=True)
|
||||||
|
req.environ['manila.context'].project_id = PROJ2_UUID
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_admin_ispublic_none(self):
|
||||||
|
expected = {'share_types': [
|
||||||
|
{'id': '0'}, {'id': '1'}, {'id': '2'}, {'id': '3'},
|
||||||
|
]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=all',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_no_admin_default(self):
|
||||||
|
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
||||||
|
use_admin_context=False)
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_no_admin_ispublic_true(self):
|
||||||
|
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
||||||
|
use_admin_context=False)
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_no_admin_ispublic_false(self):
|
||||||
|
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||||
|
use_admin_context=False)
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_list_type_with_no_admin_ispublic_none(self):
|
||||||
|
expected = {'share_types': [{'id': '0'}, {'id': '1'}]}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=all',
|
||||||
|
use_admin_context=False)
|
||||||
|
|
||||||
|
result = self.controller.index(req)
|
||||||
|
|
||||||
|
self.assertShareTypeListEqual(expected['share_types'],
|
||||||
|
result['share_types'])
|
||||||
|
|
||||||
|
def test_add_project_access(self):
|
||||||
|
def stub_add_share_type_access(context, type_id, project_id):
|
||||||
|
self.assertEqual('3', type_id, "type_id")
|
||||||
|
self.assertEqual(PROJ2_UUID, project_id, "project_id")
|
||||||
|
self.mock_object(db, 'share_type_access_add',
|
||||||
|
stub_add_share_type_access)
|
||||||
|
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
result = self.controller._add_project_access(req, '3', body)
|
||||||
|
|
||||||
|
self.assertEqual(202, result.status_code)
|
||||||
|
|
||||||
|
def test_add_project_access_with_no_admin_user(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||||
|
use_admin_context=False)
|
||||||
|
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller._add_project_access,
|
||||||
|
req, '2', body)
|
||||||
|
|
||||||
|
def test_add_project_access_with_already_added_access(self):
|
||||||
|
def stub_add_share_type_access(context, type_id, project_id):
|
||||||
|
raise exception.ShareTypeAccessExists(share_type_id=type_id,
|
||||||
|
project_id=project_id)
|
||||||
|
self.mock_object(db, 'share_type_access_add',
|
||||||
|
stub_add_share_type_access)
|
||||||
|
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPConflict,
|
||||||
|
self.controller._add_project_access,
|
||||||
|
req, '3', body)
|
||||||
|
|
||||||
|
def test_add_project_access_to_public_share_type(self):
|
||||||
|
share_type_id = '3'
|
||||||
|
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||||
|
self.mock_object(share_types, 'get_share_type',
|
||||||
|
mock.Mock(return_value={"is_public": True}))
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller._add_project_access,
|
||||||
|
req, share_type_id, body)
|
||||||
|
|
||||||
|
share_types.get_share_type.assert_called_once_with(
|
||||||
|
mock.ANY, share_type_id)
|
||||||
|
|
||||||
|
def test_remove_project_access_with_bad_access(self):
|
||||||
|
def stub_remove_share_type_access(context, type_id, project_id):
|
||||||
|
raise exception.ShareTypeAccessNotFound(share_type_id=type_id,
|
||||||
|
project_id=project_id)
|
||||||
|
self.mock_object(db, 'share_type_access_remove',
|
||||||
|
stub_remove_share_type_access)
|
||||||
|
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||||
|
use_admin_context=True)
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPNotFound,
|
||||||
|
self.controller._remove_project_access,
|
||||||
|
req, '3', body)
|
||||||
|
|
||||||
|
def test_remove_project_access_with_no_admin_user(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||||
|
use_admin_context=False)
|
||||||
|
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller._remove_project_access,
|
||||||
|
req, '2', body)
|
||||||
|
@ -20,7 +20,7 @@ import mock
|
|||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from manila.api.contrib import types_extra_specs
|
from manila.api.v1 import share_types_extra_specs
|
||||||
from manila.common import constants
|
from manila.common import constants
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila import test
|
from manila import test
|
||||||
@ -89,7 +89,8 @@ class ShareTypesExtraSpecsTest(test.TestCase):
|
|||||||
self.flags(host='fake')
|
self.flags(host='fake')
|
||||||
self.mock_object(manila.db, 'share_type_get', share_type_get)
|
self.mock_object(manila.db, 'share_type_get', share_type_get)
|
||||||
self.api_path = '/v2/fake/os-share-types/1/extra_specs'
|
self.api_path = '/v2/fake/os-share-types/1/extra_specs'
|
||||||
self.controller = types_extra_specs.ShareTypeExtraSpecsController()
|
self.controller = (
|
||||||
|
share_types_extra_specs.ShareTypeExtraSpecsController())
|
||||||
|
|
||||||
"""to reset notifier drivers left over from other api/contrib tests"""
|
"""to reset notifier drivers left over from other api/contrib tests"""
|
||||||
self.addCleanup(fake_notifier.reset)
|
self.addCleanup(fake_notifier.reset)
|
@ -37,6 +37,17 @@
|
|||||||
"share_type:index": "rule:default",
|
"share_type:index": "rule:default",
|
||||||
"share_type:show": "rule:default",
|
"share_type:show": "rule:default",
|
||||||
"share_type:default": "rule:default",
|
"share_type:default": "rule:default",
|
||||||
|
"share_type:create": "rule:default",
|
||||||
|
"share_type:delete": "rule:default",
|
||||||
|
"share_type:add_project_access": "rule:admin_api",
|
||||||
|
"share_type:list_project_access": "rule:admin_api",
|
||||||
|
"share_type:remove_project_access": "rule:admin_api",
|
||||||
|
|
||||||
|
"share_types_extra_spec:create": "rule:default",
|
||||||
|
"share_types_extra_spec:update": "rule:default",
|
||||||
|
"share_types_extra_spec:show": "rule:default",
|
||||||
|
"share_types_extra_spec:index": "rule:default",
|
||||||
|
"share_types_extra_spec:delete": "rule:default",
|
||||||
|
|
||||||
"share_instance:index": "rule:admin_api",
|
"share_instance:index": "rule:admin_api",
|
||||||
"share_instance:show": "rule:admin_api",
|
"share_instance:show": "rule:admin_api",
|
||||||
@ -62,11 +73,6 @@
|
|||||||
"share:get_share_metadata": "",
|
"share:get_share_metadata": "",
|
||||||
"share:delete_share_metadata": "",
|
"share:delete_share_metadata": "",
|
||||||
"share:update_share_metadata": "",
|
"share:update_share_metadata": "",
|
||||||
"share_extension:types_manage": "",
|
|
||||||
"share_extension:types_extra_specs": "",
|
|
||||||
"share_extension:share_type_access": "",
|
|
||||||
"share_extension:share_type_access:addProjectAccess": "rule:admin_api",
|
|
||||||
"share_extension:share_type_access:removeProjectAccess": "rule:admin_api",
|
|
||||||
"share_extension:availability_zones": "",
|
"share_extension:availability_zones": "",
|
||||||
|
|
||||||
"security_service:index": "",
|
"security_service:index": "",
|
||||||
|
Loading…
Reference in New Issue
Block a user