Add Consistency Groups API

This patch adds the /consistency-groups and /cgsnapshots
endpoints as well as AdminActions for both.

Partially implements bp manila-consistency-groups

APIImpact

Change-Id: I5fd0d3341766fdba4d92f4a43c3d1186e7a4b38e
This commit is contained in:
Alex Meade 2015-08-12 13:00:58 -04:00
parent f3a761f06b
commit 680fd50d3e
31 changed files with 3757 additions and 24 deletions

View File

@ -40,6 +40,10 @@
"share_extension:snapshot_admin_actions:reset_status": "rule:admin_api",
"share_extension:share_instance_admin_actions:force_delete": "rule:admin_api",
"share_extension:share_instance_admin_actions:reset_status": "rule:admin_api",
"share_extension:consistency_group_admin_actions:force_delete": "rule:admin_api",
"share_extension:consistency_group_admin_actions:reset_status": "rule:admin_api",
"share_extension:cgsnapshot_admin_actions:force_delete": "rule:admin_api",
"share_extension:cgsnapshot_admin_actions:reset_status": "rule:admin_api",
"share_extension:services": "rule:admin_api",
"share_extension:availability_zones": "",
@ -78,5 +82,16 @@
"share_network:get_all_share_networks": "rule:admin_api",
"scheduler_stats:pools:index": "rule:admin_api",
"scheduler_stats:pools:detail": "rule:admin_api"
"scheduler_stats:pools:detail": "rule:admin_api",
"consistency_group:create" : "rule:default",
"consistency_group:delete": "rule:default",
"consistency_group:update": "rule:default",
"consistency_group:get": "rule:default",
"consistency_group:get_all": "rule:default",
"consistency_group:create_cgsnapshot" : "rule:default",
"consistency_group:delete_cgsnapshot": "rule:default",
"consistency_group:get_cgsnapshot": "rule:default",
"consistency_group:get_all_cgsnapshots": "rule:default"
}

View File

@ -22,6 +22,8 @@ import six
from six.moves.urllib import parse
import webob
from manila.api.openstack import api_version_request as api_version
from manila.api.openstack import versioned_method
from manila.i18n import _
api_common_opts = [
@ -201,6 +203,7 @@ class ViewBuilder(object):
"""Model API responses as dictionaries."""
_collection_name = None
_detail_version_modifiers = []
def _get_links(self, request, identifier):
return [{"rel": "self",
@ -262,6 +265,43 @@ class ViewBuilder(object):
url_parts[0:2] = prefix_parts[0:2]
return parse.urlunsplit(url_parts)
def update_versioned_resource_dict(self, request, resource_dict, resource):
"""Updates teh given resource dict for the given request version.
This method calls every method, that is applicable to the request
version, in _detail_version_modifiers.
"""
for method_name in self._detail_version_modifiers:
method = getattr(self, method_name)
if request.api_version_request.matches_versioned_method(method):
method.func(self, resource_dict, resource)
@classmethod
def versioned_method(cls, min_ver, max_ver=None, experimental=False):
"""Decorator for versioning API methods.
:param min_ver: string representing minimum version
:param max_ver: optional string representing maximum version
:param experimental: flag indicating an API is experimental and is
subject to change or removal at any time
"""
def decorator(f):
obj_min_ver = api_version.APIVersionRequest(min_ver)
if max_ver:
obj_max_ver = api_version.APIVersionRequest(max_ver)
else:
obj_max_ver = api_version.APIVersionRequest()
# Add to list of versioned methods registered
func_name = f.__name__
new_func = versioned_method.VersionedMethod(
func_name, obj_min_ver, obj_max_ver, experimental, f)
return new_func
return decorator
def remove_invalid_options(context, search_options, allowed_search_options):
"""Remove search options that are not valid for non-admin API/context."""

View File

@ -20,6 +20,7 @@ from webob import exc
from manila.api import extensions
from manila.api.openstack import wsgi
from manila.common import constants
import manila.consistency_group.api as cg_api
from manila import db
from manila import exception
from manila import share
@ -42,8 +43,9 @@ class AdminController(wsgi.Controller):
def __init__(self, *args, **kwargs):
super(AdminController, self).__init__(*args, **kwargs)
self.resource_name = self.collection.rstrip('s')
self.resource_name = self.collection.rstrip('s').replace('-', '_')
self.share_api = share.API()
self.cg_api = cg_api.API()
def _update(self, *args, **kwargs):
raise NotImplementedError()
@ -143,18 +145,80 @@ class SnapshotAdminController(AdminController):
return self.share_api.delete_snapshot(*args, **kwargs)
class CGAdminController(AdminController):
"""AdminController for Consistency Groups."""
collection = 'consistency-groups'
def __init__(self, *args, **kwargs):
super(CGAdminController, self).__init__(*args, **kwargs)
self.cg_api = cg_api.API()
def _update(self, *args, **kwargs):
db.consistency_group_update(*args, **kwargs)
def _get(self, *args, **kwargs):
return self.cg_api.get(*args, **kwargs)
def _delete(self, context, resource, force=True):
db.consistency_group_destroy(context.elevated(), resource['id'])
@wsgi.action('os-reset_status')
@wsgi.response(202)
@wsgi.Controller.api_version('1.5', experimental=True)
def cg_reset_status(self, req, id, body):
super(CGAdminController, self)._reset_status(req, id, body)
@wsgi.action('os-force_delete')
@wsgi.response(202)
@wsgi.Controller.api_version('1.5', experimental=True)
def cg_force_delete(self, req, id, body):
super(CGAdminController, self)._force_delete(req, id, body)
class CGSnapshotAdminController(AdminController):
"""AdminController for CGSnapshots."""
collection = 'cgsnapshots'
def __init__(self, *args, **kwargs):
super(CGSnapshotAdminController, self).__init__(*args, **kwargs)
self.cg_api = cg_api.API()
def _update(self, *args, **kwargs):
db.cgsnapshot_update(*args, **kwargs)
def _get(self, *args, **kwargs):
return self.cg_api.get_cgsnapshot(*args, **kwargs)
def _delete(self, context, resource, force=True):
db.cgsnapshot_destroy(context.elevated(), resource['id'])
@wsgi.action('os-reset_status')
@wsgi.response(202)
@wsgi.Controller.api_version('1.5', experimental=True)
def cgsnapshot_reset_status(self, req, id, body):
super(CGSnapshotAdminController, self)._reset_status(req, id, body)
@wsgi.action('os-force_delete')
@wsgi.response(202)
@wsgi.Controller.api_version('1.5', experimental=True)
def cgsnapshot_force_delete(self, req, id, body):
super(CGSnapshotAdminController, self)._force_delete(req, id, body)
class Admin_actions(extensions.ExtensionDescriptor):
"""Enable admin actions."""
name = "AdminActions"
alias = "os-admin-actions"
updated = "2015-08-03T00:00:00+00:00"
updated = "2015-09-01T00:00:00+00:00"
def get_controller_extensions(self):
exts = []
controllers = (ShareAdminController, SnapshotAdminController,
ShareInstancesAdminController)
for class_ in controllers:
for class_ in (ShareAdminController, SnapshotAdminController,
ShareInstancesAdminController,
CGAdminController, CGSnapshotAdminController):
controller = class_()
extension = extensions.ControllerExtension(
self, class_.collection, controller)

View File

@ -49,6 +49,7 @@ REST_API_VERSION_HISTORY = """
* 1.2 - Share create() doesn't ignore availability_zone field of share.
* 1.3 - Snapshots become optional feature.
* 1.4 - Share instances admin API
* 1.5 - Consistency Group support
"""
@ -56,7 +57,7 @@ REST_API_VERSION_HISTORY = """
# The default api version request is defined to be the
# the minimum version of the API supported.
_MIN_API_VERSION = "1.0"
_MAX_API_VERSION = "1.4"
_MAX_API_VERSION = "1.5"
DEFAULT_API_VERSION = _MIN_API_VERSION
@ -79,11 +80,11 @@ class APIVersionRequest(utils.ComparableMixin):
API microversions.
"""
def __init__(self, version_string=None):
def __init__(self, version_string=None, experimental=False):
"""Create an API version request object."""
self._ver_major = None
self._ver_minor = None
self._experimental = False
self._experimental = experimental
if version_string is not None:
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",

View File

@ -44,3 +44,9 @@ user documentation.
1.4
---
Share instances admin API and update of Admin Actions extension.
1.5
---
Consistency groups support. /consistency-groups and /cgsnapshots are
implemented. AdminActions 'os-force_delete and' 'os-reset_status' have been
updated for both new resources.

View File

@ -0,0 +1,198 @@
# Copyright 2015 Alex Meade
# 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.
"""The consistency groups snapshot API."""
from oslo_log import log
from oslo_utils import uuidutils
import six
import webob
from webob import exc
from manila.api import common
from manila.api.openstack import wsgi
import manila.api.views.cgsnapshots as cg_views
import manila.consistency_group.api as cg_api
from manila import exception
from manila.i18n import _
from manila.i18n import _LI
LOG = log.getLogger(__name__)
class CGSnapshotController(wsgi.Controller):
"""The Consistency Group Snapshots API controller for the OpenStack API."""
_view_builder_class = cg_views.CGSnapshotViewBuilder
def __init__(self):
super(CGSnapshotController, self).__init__()
self.cg_api = cg_api.API()
@wsgi.Controller.api_version('1.5', experimental=True)
def show(self, req, id):
"""Return data about the given cgsnapshot."""
context = req.environ['manila.context']
try:
cg = self.cg_api.get_cgsnapshot(context, id)
except exception.NotFound:
msg = _("Consistency group snapshot %s not found.") % id
raise exc.HTTPNotFound(explanation=msg)
return self._view_builder.detail(req, cg)
@wsgi.Controller.api_version('1.5', experimental=True)
def delete(self, req, id):
"""Delete a cgsnapshot."""
context = req.environ['manila.context']
LOG.info(_LI("Delete consistency group snapshot with id: %s"), id,
context=context)
try:
snap = self.cg_api.get_cgsnapshot(context, id)
except exception.NotFound:
msg = _("Consistency group snapshot %s not found.") % id
raise exc.HTTPNotFound(explanation=msg)
try:
self.cg_api.delete_cgsnapshot(context, snap)
except exception.InvalidCGSnapshot as e:
raise exc.HTTPConflict(explanation=six.text_type(e))
return webob.Response(status_int=202)
@wsgi.Controller.api_version('1.5', experimental=True)
def index(self, req):
"""Returns a summary list of cgsnapshots."""
return self._get_cgs(req, is_detail=False)
@wsgi.Controller.api_version('1.5', experimental=True)
def detail(self, req):
"""Returns a detailed list of cgsnapshots."""
return self._get_cgs(req, is_detail=True)
def _get_cgs(self, req, is_detail):
"""Returns a list of cgsnapshots."""
context = req.environ['manila.context']
search_opts = {}
search_opts.update(req.GET)
# Remove keys that are not related to cg attrs
search_opts.pop('limit', None)
search_opts.pop('offset', None)
snaps = self.cg_api.get_all_cgsnapshots(
context, detailed=is_detail, search_opts=search_opts)
limited_list = common.limited(snaps, req)
if is_detail:
snaps = self._view_builder.detail_list(req, limited_list)
else:
snaps = self._view_builder.summary_list(req, limited_list)
return snaps
@wsgi.Controller.api_version('1.5', experimental=True)
def update(self, req, id, body):
"""Update a cgsnapshot."""
context = req.environ['manila.context']
if not self.is_valid_body(body, 'cgsnapshot'):
msg = _("'cgsnapshot' is missing from the request body")
raise exc.HTTPBadRequest(explanation=msg)
cg_data = body['cgsnapshot']
valid_update_keys = {
'name',
'description',
}
invalid_fields = set(cg_data.keys()) - valid_update_keys
if invalid_fields:
msg = _("The fields %s are invalid or not allowed to be updated.")
raise exc.HTTPBadRequest(explanation=msg % invalid_fields)
try:
cg = self.cg_api.get_cgsnapshot(context, id)
except exception.NotFound:
msg = _("Consistency group snapshot %s not found.") % id
raise exc.HTTPNotFound(explanation=msg)
cg = self.cg_api.update_cgsnapshot(context, cg, cg_data)
return self._view_builder.detail(req, cg)
@wsgi.Controller.api_version('1.5', experimental=True)
@wsgi.response(202)
def create(self, req, body):
"""Creates a new cgsnapshot."""
context = req.environ['manila.context']
if not self.is_valid_body(body, 'cgsnapshot'):
msg = _("'cgsnapshot' is missing from the request body")
raise exc.HTTPBadRequest(explanation=msg)
cgsnapshot = body.get('cgsnapshot')
if not cgsnapshot.get('consistency_group_id'):
msg = _("Must supply 'consistency_group_id' attribute.")
raise exc.HTTPBadRequest(explanation=msg)
consistency_group_id = cgsnapshot.get('consistency_group_id')
if (consistency_group_id and
not uuidutils.is_uuid_like(consistency_group_id)):
msg = _("The 'consistency_group_id' attribute must be a uuid.")
raise exc.HTTPBadRequest(explanation=six.text_type(msg))
kwargs = {"consistency_group_id": consistency_group_id}
if 'name' in cgsnapshot:
kwargs['name'] = cgsnapshot.get('name')
if 'description' in cgsnapshot:
kwargs['description'] = cgsnapshot.get('description')
try:
new_snapshot = self.cg_api.create_cgsnapshot(context, **kwargs)
except exception.ConsistencyGroupNotFound as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
except exception.InvalidConsistencyGroup as e:
raise exc.HTTPConflict(explanation=six.text_type(e))
return self._view_builder.detail(req, dict(six.iteritems(
new_snapshot)))
@wsgi.Controller.api_version('1.5', experimental=True)
def members(self, req, id):
"""Returns a list of cgsnapshot members."""
context = req.environ['manila.context']
search_opts = {}
search_opts.update(req.GET)
# Remove keys that are not related to cg attrs
search_opts.pop('limit', None)
search_opts.pop('offset', None)
snaps = self.cg_api.get_all_cgsnapshot_members(context, id)
limited_list = common.limited(snaps, req)
snaps = self._view_builder.member_list(req, limited_list)
return snaps
def create_resource():
return wsgi.Resource(CGSnapshotController())

View File

@ -0,0 +1,211 @@
# Copyright 2015 Alex Meade
# 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.
"""The consistency groups API."""
from oslo_log import log
from oslo_utils import uuidutils
import six
import webob
from webob import exc
from manila.api import common
from manila.api.openstack import wsgi
import manila.api.views.consistency_groups as cg_views
import manila.consistency_group.api as cg_api
from manila import exception
from manila.i18n import _
from manila.i18n import _LI
from manila.share import share_types
LOG = log.getLogger(__name__)
class CGController(wsgi.Controller):
"""The Consistency Groups API controller for the OpenStack API."""
_view_builder_class = cg_views.CGViewBuilder
def __init__(self):
super(CGController, self).__init__()
self.cg_api = cg_api.API()
@wsgi.Controller.api_version('1.5', experimental=True)
def show(self, req, id):
"""Return data about the given CG."""
context = req.environ['manila.context']
try:
cg = self.cg_api.get(context, id)
except exception.NotFound:
msg = _("Consistency group %s not found.") % id
raise exc.HTTPNotFound(explanation=msg)
return self._view_builder.detail(req, cg)
@wsgi.Controller.api_version('1.5', experimental=True)
def delete(self, req, id):
"""Delete a CG."""
context = req.environ['manila.context']
LOG.info(_LI("Delete consistency group with id: %s"), id,
context=context)
try:
cg = self.cg_api.get(context, id)
except exception.NotFound:
msg = _("Consistency group %s not found.") % id
raise exc.HTTPNotFound(explanation=msg)
try:
self.cg_api.delete(context, cg)
except exception.InvalidConsistencyGroup as e:
raise exc.HTTPConflict(explanation=six.text_type(e))
return webob.Response(status_int=202)
@wsgi.Controller.api_version('1.5', experimental=True)
def index(self, req):
"""Returns a summary list of shares."""
return self._get_cgs(req, is_detail=False)
@wsgi.Controller.api_version('1.5', experimental=True)
def detail(self, req):
"""Returns a detailed list of shares."""
return self._get_cgs(req, is_detail=True)
def _get_cgs(self, req, is_detail):
"""Returns a list of shares, transformed through view builder."""
context = req.environ['manila.context']
search_opts = {}
search_opts.update(req.GET)
# Remove keys that are not related to cg attrs
search_opts.pop('limit', None)
search_opts.pop('offset', None)
cgs = self.cg_api.get_all(
context, detailed=is_detail, search_opts=search_opts)
limited_list = common.limited(cgs, req)
if is_detail:
cgs = self._view_builder.detail_list(req, limited_list)
else:
cgs = self._view_builder.summary_list(req, limited_list)
return cgs
@wsgi.Controller.api_version('1.5', experimental=True)
def update(self, req, id, body):
"""Update a share."""
context = req.environ['manila.context']
if not self.is_valid_body(body, 'consistency_group'):
msg = _("'consistency_group' is missing from the request body.")
raise exc.HTTPBadRequest(explanation=msg)
cg_data = body['consistency_group']
valid_update_keys = {
'name',
'description',
}
invalid_fields = set(cg_data.keys()) - valid_update_keys
if invalid_fields:
msg = _("The fields %s are invalid or not allowed to be updated.")
raise exc.HTTPBadRequest(explanation=msg % invalid_fields)
try:
cg = self.cg_api.get(context, id)
except exception.NotFound:
msg = _("Consistency group %s not found.") % id
raise exc.HTTPNotFound(explanation=msg)
cg = self.cg_api.update(context, cg, cg_data)
return self._view_builder.detail(req, cg)
@wsgi.Controller.api_version('1.5', experimental=True)
@wsgi.response(202)
def create(self, req, body):
"""Creates a new share."""
context = req.environ['manila.context']
if not self.is_valid_body(body, 'consistency_group'):
msg = _("'consistency_group' is missing from the request body.")
raise exc.HTTPBadRequest(explanation=msg)
cg = body['consistency_group']
valid_fields = {'name', 'description', 'share_types',
'source_cgsnapshot_id', 'share_network_id'}
invalid_fields = set(cg.keys()) - valid_fields
if invalid_fields:
msg = _("The fields %s are invalid.") % invalid_fields
raise exc.HTTPBadRequest(explanation=msg)
if 'share_types' in cg and 'source_cgsnapshot_id' in cg:
msg = _("Cannot supply both 'share_types' and "
"'source_cgsnapshot_id' attributes.")
raise exc.HTTPBadRequest(explanation=msg)
if not cg.get('share_types') and 'source_cgsnapshot_id' not in cg:
default_share_type = share_types.get_default_share_type()
if default_share_type:
cg['share_types'] = [default_share_type['id']]
else:
msg = _("Must specify at least one share type as a default "
"share type has not been configured.")
raise exc.HTTPBadRequest(explanation=msg)
kwargs = {}
if 'name' in cg:
kwargs['name'] = cg.get('name')
if 'description' in cg:
kwargs['description'] = cg.get('description')
_share_types = cg.get('share_types')
if _share_types:
if not all([uuidutils.is_uuid_like(st) for st in _share_types]):
msg = _("The 'share_types' attribute must be a list of uuids")
raise exc.HTTPBadRequest(explanation=msg)
kwargs['share_type_ids'] = _share_types
if 'source_cgsnapshot_id' in cg:
source_cgsnapshot_id = cg.get('source_cgsnapshot_id')
if not uuidutils.is_uuid_like(source_cgsnapshot_id):
msg = _("The 'source_cgsnapshot_id' attribute must be a uuid.")
raise exc.HTTPBadRequest(explanation=six.text_type(msg))
kwargs['source_cgsnapshot_id'] = source_cgsnapshot_id
if 'share_network_id' in cg:
share_network_id = cg.get('share_network_id')
if not uuidutils.is_uuid_like(share_network_id):
msg = _("The 'share_network_id' attribute must be a uuid.")
raise exc.HTTPBadRequest(explanation=six.text_type(msg))
kwargs['share_network_id'] = share_network_id
try:
new_cg = self.cg_api.create(context, **kwargs)
except exception.InvalidCGSnapshot as e:
raise exc.HTTPConflict(explanation=six.text_type(e))
except (exception.CGSnapshotNotFound, exception.InvalidInput) as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
return self._view_builder.detail(req, dict(six.iteritems(new_cg)))
def create_resource():
return wsgi.Resource(CGController())

View File

@ -23,6 +23,8 @@ from oslo_log import log
from manila.api import extensions
import manila.api.openstack
from manila.api.v1 import cgsnapshots
from manila.api.v1 import consistency_groups
from manila.api.v1 import limits
from manila.api.v1 import scheduler_stats
from manila.api.v1 import security_service
@ -134,3 +136,20 @@ class APIRouter(manila.api.openstack.APIRouter):
controller=self.resources['scheduler_stats'],
action='pools_detail',
conditions={'method': ['GET']})
self.resources['consistency-groups'] = (
consistency_groups.create_resource())
mapper.resource('consistency-group', 'consistency-groups',
controller=self.resources['consistency-groups'],
collection={'detail': 'GET'})
mapper.connect('consistency-groups',
'/{project_id}/consistency-groups/{id}/action',
controller=self.resources['consistency-groups'],
action='action',
conditions={"action": ['POST']})
self.resources['cgsnapshots'] = cgsnapshots.create_resource()
mapper.resource('cgsnapshot', 'cgsnapshots',
controller=self.resources['cgsnapshots'],
collection={'detail': 'GET'},
member={'members': 'GET', 'action': 'POST'})

View File

@ -80,6 +80,16 @@ class ShareNetworkController(wsgi.Controller):
'len': len(share_instances)}
LOG.error(msg)
raise exc.HTTPConflict(explanation=msg)
# NOTE(ameade): Do not allow deletion of share network used by CG
cg_count = db_api.count_consistency_groups_in_share_network(context,
id)
if cg_count:
msg = _("Can not delete share network %(id)s, it has %(len)s "
"consistency group(s).") % {'id': id, 'len': cg_count}
LOG.error(msg)
raise exc.HTTPConflict(explanation=msg)
for share_server in share_network['share_servers']:
self.share_rpcapi.delete_share_server(context, share_server)
db_api.share_network_delete(context, id)

View File

@ -64,6 +64,22 @@ class ShareController(wsgi.Controller):
try:
share = self.share_api.get(context, id)
# NOTE(ameade): If the share is in a consistency group, we require
# it's id be specified as a param.
if share.get('consistency_group_id'):
consistency_group_id = req.params.get('consistency_group_id')
if (share.get('consistency_group_id') and
not consistency_group_id):
msg = _("Must provide 'consistency_group_id' as a request "
"parameter when deleting a share in a consistency "
"group.")
raise exc.HTTPBadRequest(explanation=msg)
elif consistency_group_id != share.get('consistency_group_id'):
msg = _("The specified 'consistency_group_id' does not "
"match the consistency group id of the share.")
raise exc.HTTPBadRequest(explanation=msg)
self.share_api.delete(context, share)
except exception.NotFound:
raise exc.HTTPNotFound()
@ -133,6 +149,7 @@ class ShareController(wsgi.Controller):
'display_name', 'status', 'share_server_id', 'volume_type_id',
'share_type_id', 'snapshot_id', 'host', 'share_network_id',
'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir',
'consistency_group_id', 'cgsnapshot_id'
)
def update(self, req, id, body):
@ -162,14 +179,18 @@ class ShareController(wsgi.Controller):
share.update(update_dict)
return self._view_builder.detail(req, share)
@wsgi.Controller.api_version("1.3")
@wsgi.Controller.api_version("1.5")
def create(self, req, body):
return self._create(req, body)
@wsgi.Controller.api_version("1.0", "1.2") # noqa
def create(self, req, body):
@wsgi.Controller.api_version("1.0", "1.4") # noqa
def create(self, req, body): # pylint: disable=E0102
# Remove consistency group attributes
share = body.get('share', {})
if 'consistency_group_id' in share:
del body['share']['consistency_group_id']
share = self._create(req, body)
share.pop('snapshot_support', None)
return share
def _create(self, req, body):
@ -211,6 +232,7 @@ class ShareController(wsgi.Controller):
'availability_zone': availability_zone,
'metadata': share.get('metadata'),
'is_public': share.get('is_public', False),
'consistency_group_id': share.get('consistency_group_id')
}
snapshot_id = share.get('snapshot_id')

View File

@ -0,0 +1,100 @@
# Copyright 2015 Alex Meade
# 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.
"""The consistency groups snapshot API."""
from oslo_log import log
from manila.api import common
LOG = log.getLogger(__name__)
class CGSnapshotViewBuilder(common.ViewBuilder):
"""Model a cgsnapshot API response as a python dictionary."""
_collection_name = 'cgsnapshot'
def summary_list(self, request, cgs):
"""Show a list of cgsnapshots without many details."""
return self._list_view(self.summary, request, cgs)
def detail_list(self, request, cgs):
"""Detailed view of a list of cgsnapshots."""
return self._list_view(self.detail, request, cgs)
def member_list(self, request, members):
members_list = []
for member in members:
member_dict = {
'id': member.get('id'),
'created_at': member.get('created_at'),
'size': member.get('size'),
'share_protocol': member.get('share_proto'),
'project_id': member.get('project_id'),
'share_type_id': member.get('share_type_id'),
'cgsnapshot_id': member.get('cgsnapshot_id'),
'share_id': member.get('share_id'),
}
members_list.append(member_dict)
members_links = self._get_collection_links(request,
members,
'cgsnapshot_id')
members_dict = dict(cgsnapshot_members=members_list)
if members_links:
members_dict['cgsnapshot_members_links'] = members_links
return members_dict
def summary(self, request, cg):
"""Generic, non-detailed view of a cgsnapshot."""
return {
'cgsnapshot': {
'id': cg.get('id'),
'name': cg.get('name'),
'links': self._get_links(request, cg['id'])
}
}
def detail(self, request, cg):
"""Detailed view of a single cgsnapshot."""
cg_dict = {
'id': cg.get('id'),
'name': cg.get('name'),
'created_at': cg.get('created_at'),
'status': cg.get('status'),
'description': cg.get('description'),
'project_id': cg.get('project_id'),
'consistency_group_id': cg.get('consistency_group_id'),
'links': self._get_links(request, cg['id']),
}
return {'cgsnapshot': cg_dict}
def _list_view(self, func, request, snaps):
"""Provide a view for a list of cgsnapshots."""
snap_list = [func(request, snap)['cgsnapshot']
for snap in snaps]
snaps_links = self._get_collection_links(request,
snaps,
self._collection_name)
snaps_dict = dict(cgsnapshots=snap_list)
if snaps_links:
snaps_dict['cgsnapshot_links'] = snaps_links
return snaps_dict

View File

@ -0,0 +1,82 @@
# Copyright 2015 Alex Meade
# 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.
"""The consistency groups API."""
from oslo_log import log
from manila.api import common
LOG = log.getLogger(__name__)
class CGViewBuilder(common.ViewBuilder):
"""Model a consistency group API response as a python dictionary."""
_collection_name = 'consistency_groups'
def summary_list(self, request, cgs):
"""Show a list of consistency groups without many details."""
return self._list_view(self.summary, request, cgs)
def detail_list(self, request, cgs):
"""Detailed view of a list of consistency groups."""
return self._list_view(self.detail, request, cgs)
def summary(self, request, cg):
"""Generic, non-detailed view of a consistency group."""
return {
'consistency_group': {
'id': cg.get('id'),
'name': cg.get('name'),
'links': self._get_links(request, cg['id'])
}
}
def detail(self, request, cg):
"""Detailed view of a single consistency group."""
context = request.environ['manila.context']
cg_dict = {
'id': cg.get('id'),
'name': cg.get('name'),
'created_at': cg.get('created_at'),
'status': cg.get('status'),
'description': cg.get('description'),
'project_id': cg.get('project_id'),
'host': cg.get('host'),
'source_cgsnapshot_id': cg.get('source_cgsnapshot_id'),
'share_network_id': cg.get('share_network_id'),
'share_types': [st['share_type_id'] for st in cg.get(
'share_types')],
'links': self._get_links(request, cg['id']),
}
if context.is_admin:
cg_dict['share_server_id'] = cg_dict.get('share_server_id')
return {'consistency_group': cg_dict}
def _list_view(self, func, request, shares):
"""Provide a view for a list of consistency groups."""
cg_list = [func(request, share)['consistency_group']
for share in shares]
cgs_links = self._get_collection_links(request,
shares,
self._collection_name)
cgs_dict = dict(consistency_groups=cg_list)
if cgs_links:
cgs_dict['consistency_groups_links'] = cgs_links
return cgs_dict

View File

@ -20,6 +20,7 @@ class ViewBuilder(common.ViewBuilder):
"""Model a server API response as a python dictionary."""
_collection_name = 'shares'
_detail_version_modifiers = ["add_consistency_group_fields"]
def summary_list(self, request, shares):
"""Show a list of shares without many details."""
@ -76,10 +77,20 @@ class ViewBuilder(common.ViewBuilder):
'is_public': share.get('is_public'),
'export_locations': export_locations,
}
self.update_versioned_resource_dict(request, share_dict, share)
if context.is_admin:
share_dict['share_server_id'] = share.get('share_server_id')
return {'share': share_dict}
@common.ViewBuilder.versioned_method("1.5")
def add_consistency_group_fields(self, share_dict, share):
share_dict['consistency_group_id'] = share.get(
'consistency_group_id')
share_dict['source_cgsnapshot_member_id'] = share.get(
'source_cgsnapshot_member_id')
def _list_view(self, func, request, shares):
"""Provide a view for a list of shares."""
shares_list = [func(request, share)['share'] for share in shares]

View File

View File

@ -0,0 +1,343 @@
# Copyright (c) 2015 Alex Meade
# 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.
"""
Handles all requests relating to consistency groups.
"""
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import strutils
import six
from manila.common import constants
from manila.db import base
from manila import exception
from manila.i18n import _
from manila import policy
from manila.scheduler import rpcapi as scheduler_rpcapi
from manila import share
from manila.share import rpcapi as share_rpcapi
from manila.share import share_types
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class API(base.Base):
"""API for interacting with the share manager."""
def __init__(self, db_driver=None):
self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
self.share_rpcapi = share_rpcapi.ShareAPI()
self.share_api = share.API()
super(API, self).__init__(db_driver)
def create(self, context, name=None, description=None,
share_type_ids=None, source_cgsnapshot_id=None,
share_network_id=None):
"""Create new consistency group."""
policy.check_policy(context, 'consistency_group', 'create')
cgsnapshot = None
original_cg = None
if source_cgsnapshot_id:
cgsnapshot = self.db.cgsnapshot_get(context, source_cgsnapshot_id)
if cgsnapshot['status'] != constants.STATUS_AVAILABLE:
msg = (_("Consistency group snapshot status must be %s")
% constants.STATUS_AVAILABLE)
raise exception.InvalidCGSnapshot(reason=msg)
original_cg = self.db.consistency_group_get(context, cgsnapshot[
'consistency_group_id'])
share_type_ids = [s['share_type_id'] for s in original_cg[
'share_types']]
# Get share_type_objects
share_type_objects = []
driver_handles_share_servers = None
for share_type_id in (share_type_ids or []):
try:
share_type_object = share_types.get_share_type(
context, share_type_id)
except exception.ShareTypeNotFound:
msg = _("Share type with id %s could not be found")
raise exception.InvalidInput(msg % share_type_id)
share_type_objects.append(share_type_object)
extra_specs = share_type_object.get('extra_specs')
if extra_specs:
share_type_handle_ss = strutils.bool_from_string(
extra_specs.get(
constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS))
if driver_handles_share_servers is None:
driver_handles_share_servers = share_type_handle_ss
elif not driver_handles_share_servers == share_type_handle_ss:
# NOTE(ameade): if the share types have conflicting values
# for driver_handles_share_servers then raise bad request
msg = _("The specified share_types cannot have "
"conflicting values for the "
"driver_handles_share_servers extra spec.")
raise exception.InvalidInput(reason=msg)
if (not share_type_handle_ss) and share_network_id:
msg = _("When using a share types with the "
"driver_handles_share_servers extra spec as "
"False, a share_network_id must not be provided.")
raise exception.InvalidInput(reason=msg)
try:
if share_network_id:
self.db.share_network_get(context, share_network_id)
except exception.ShareNetworkNotFound:
msg = _("The specified share network does not exist.")
raise exception.InvalidInput(reason=msg)
if (driver_handles_share_servers and
not (source_cgsnapshot_id or share_network_id)):
msg = _("When using a share type with the "
"driver_handles_share_servers extra spec as"
"True, a share_network_id must be provided.")
raise exception.InvalidInput(reason=msg)
options = {
'source_cgsnapshot_id': source_cgsnapshot_id,
'share_network_id': share_network_id,
'name': name,
'description': description,
'user_id': context.user_id,
'project_id': context.project_id,
'status': constants.STATUS_CREATING,
'share_types': share_type_ids
}
if original_cg:
options['host'] = original_cg['host']
cg = self.db.consistency_group_create(context, options)
try:
if cgsnapshot:
members = self.db.cgsnapshot_members_get_all(
context, source_cgsnapshot_id)
for member in members:
share_type = share_types.get_share_type(
context, member['share_type_id'])
member['share'] = self.db.share_instance_get(
context, member['share_instance_id'],
with_share_data=True)
self.share_api.create(context, member['share_proto'],
member['size'], None, None,
consistency_group_id=cg['id'],
cgsnapshot_member=member,
share_type=share_type,
share_network_id=share_network_id)
except Exception:
with excutils.save_and_reraise_exception():
self.db.consistency_group_destroy(context.elevated(), cg['id'])
request_spec = {'consistency_group_id': cg['id']}
request_spec.update(options)
request_spec['share_types'] = share_type_objects
if cgsnapshot and original_cg:
self.share_rpcapi.create_consistency_group(
context, cg, original_cg['host'])
else:
self.scheduler_rpcapi.create_consistency_group(
context, cg_id=cg['id'], request_spec=request_spec,
filter_properties={})
return cg
@policy.wrap_check_policy('consistency_group')
def delete(self, context, cg):
"""Delete consistency group."""
cg_id = cg['id']
if not cg['host']:
self.db.consistency_group_destroy(context.elevated(), cg_id)
return
statuses = (constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
if not cg['status'] in statuses:
msg = (_("Consistency group status must be one of %(statuses)s")
% {"statuses": statuses})
raise exception.InvalidConsistencyGroup(reason=msg)
# NOTE(ameade): check for cgsnapshots in the CG
if self.db.count_cgsnapshots_in_consistency_group(context, cg_id):
msg = (_("Cannot delete a consistency group with cgsnapshots"))
raise exception.InvalidConsistencyGroup(reason=msg)
# NOTE(ameade): check for shares in the CG
if self.db.count_shares_in_consistency_group(context, cg_id):
msg = (_("Cannot delete a consistency group with shares"))
raise exception.InvalidConsistencyGroup(reason=msg)
cg = self.db.consistency_group_update(
context, cg_id, {'status': constants.STATUS_DELETING})
self.share_rpcapi.delete_consistency_group(context, cg)
@policy.wrap_check_policy('consistency_group')
def update(self, context, cg, fields):
return self.db.consistency_group_update(context, cg['id'], fields)
def get(self, context, cg_id):
policy.check_policy(context, 'consistency_group', 'get')
return self.db.consistency_group_get(context, cg_id)
def get_all(self, context, detailed=True, search_opts=None):
policy.check_policy(context, 'consistency_group', 'get_all')
if search_opts is None:
search_opts = {}
LOG.debug("Searching for consistency_groups by: %s",
six.text_type(search_opts))
# Get filtered list of consistency_groups
if context.is_admin and search_opts.get('all_tenants'):
consistency_groups = self.db.consistency_group_get_all(
context, detailed=detailed)
else:
consistency_groups = self.db.consistency_group_get_all_by_project(
context, context.project_id, detailed=detailed)
return consistency_groups
def create_cgsnapshot(self, context, name=None, description=None,
consistency_group_id=None):
"""Create new cgsnapshot."""
policy.check_policy(context, 'consistency_group', 'create_cgsnapshot')
options = {
'consistency_group_id': consistency_group_id,
'name': name,
'description': description,
'user_id': context.user_id,
'project_id': context.project_id,
'status': constants.STATUS_CREATING,
}
cg = self.db.consistency_group_get(context, consistency_group_id)
# Check status of CG, must be active
if not cg['status'] == constants.STATUS_AVAILABLE:
msg = (_("Consistency group status must be %s")
% constants.STATUS_AVAILABLE)
raise exception.InvalidConsistencyGroup(reason=msg)
# Create members for every share in the CG
shares = self.db.share_get_all_by_consistency_group_id(
context, consistency_group_id)
# Check status of all shares, they must be active in order to snap
# the CG
for s in shares:
if not s['status'] == constants.STATUS_AVAILABLE:
msg = (_("Share %(s)s in consistency group must have status "
"of %(status)s in order to create a CG snapshot")
% {"s": s['id'],
"status": constants.STATUS_AVAILABLE})
raise exception.InvalidConsistencyGroup(reason=msg)
snap = self.db.cgsnapshot_create(context, options)
try:
members = []
for s in shares:
member_options = {
'cgsnapshot_id': snap['id'],
'user_id': context.user_id,
'project_id': context.project_id,
'status': constants.STATUS_CREATING,
'size': s['size'],
'share_proto': s['share_proto'],
'share_type_id': s['share_type_id'],
'share_id': s['id'],
'share_instance_id': s.instance['id']
}
member = self.db.cgsnapshot_member_create(context,
member_options)
members.append(member)
# Cast to share manager
self.share_rpcapi.create_cgsnapshot(context, snap, cg['host'])
except Exception:
with excutils.save_and_reraise_exception():
# This will delete the snapshot and all of it's members
self.db.cgsnapshot_destroy(context, snap['id'])
return snap
@policy.wrap_check_policy('consistency_group')
def delete_cgsnapshot(self, context, snap):
"""Delete consistency group snapshot."""
snap_id = snap['id']
cg = self.db.consistency_group_get(context,
snap['consistency_group_id'])
statuses = (constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
if not snap['status'] in statuses:
msg = (_("Consistency group snapshot status must be one of"
" %(statuses)s")
% {"statuses": statuses})
raise exception.InvalidCGSnapshot(reason=msg)
self.db.cgsnapshot_update(context, snap_id,
{'status': constants.STATUS_DELETING})
# Cast to share manager
self.share_rpcapi.delete_cgsnapshot(context, snap, cg['host'])
@policy.wrap_check_policy('consistency_group')
def update_cgsnapshot(self, context, cg, fields):
return self.db.cgsnapshot_update(context, cg['id'], fields)
def get_cgsnapshot(self, context, snapshot_id):
policy.check_policy(context, 'consistency_group', 'get_cgsnapshot')
return self.db.cgsnapshot_get(context, snapshot_id)
def get_all_cgsnapshots(self, context, detailed=True, search_opts=None):
policy.check_policy(context, 'consistency_group',
'get_all_cgsnapshots')
if search_opts is None:
search_opts = {}
LOG.debug("Searching for consistency group snapshots by: %s",
six.text_type(search_opts))
# Get filtered list of consistency_groups
if context.is_admin and search_opts.get('all_tenants'):
cgsnapshots = self.db.cgsnapshot_get_all(
context, detailed=detailed)
else:
cgsnapshots = self.db.cgsnapshot_get_all_by_project(
context, context.project_id, detailed=detailed)
return cgsnapshots
def get_all_cgsnapshot_members(self, context, cgsnapshot_id):
policy.check_policy(context, 'consistency_group', 'get_cgsnapshot')
members = self.db.cgsnapshot_members_get_all(context,
cgsnapshot_id)
return members

View File

@ -131,6 +131,11 @@ class PolicyNotAuthorized(NotAuthorized):
message = _("Policy doesn't allow %(action)s to be performed.")
class Conflict(ManilaException):
message = _("%(err)s")
code = 409
class Invalid(ManilaException):
message = _("Unacceptable parameters.")
code = 400

View File

@ -66,7 +66,8 @@ class API(base.Base):
def create(self, context, share_proto, size, name, description,
snapshot=None, availability_zone=None, metadata=None,
share_network_id=None, share_type=None, is_public=False):
share_network_id=None, share_type=None, is_public=False,
consistency_group_id=None, cgsnapshot_member=None):
"""Create new share."""
policy.check_policy(context, 'share', 'create')
@ -167,6 +168,49 @@ class API(base.Base):
except ValueError as e:
raise exception.InvalidParameterValue(six.text_type(e))
consistency_group = None
if consistency_group_id:
try:
consistency_group = self.db.consistency_group_get(
context, consistency_group_id)
except exception.NotFound as e:
raise exception.InvalidParameterValue(six.text_type(e))
if (not cgsnapshot_member and
not (consistency_group['status'] ==
constants.STATUS_AVAILABLE)):
params = {
'avail': constants.STATUS_AVAILABLE,
'cg_status': consistency_group['status'],
}
msg = _("Consistency group status must be %(avail)s, got"
"%(cg_status)s.") % params
raise exception.InvalidConsistencyGroup(message=msg)
if share_type_id:
cg_st_ids = [st['share_type_id'] for st in
consistency_group.get('share_types', [])]
if share_type_id not in cg_st_ids:
params = {
'type': share_type_id,
'cg': consistency_group_id
}
msg = _("The specified share type (%(type)s) is not "
"supported by the specified consistency group "
"(%(cg)s).") % params
raise exception.InvalidParameterValue(msg)
if (not consistency_group.get('share_network_id')
== share_network_id):
params = {
'net': share_network_id,
'cg': consistency_group_id
}
msg = _("The specified share network (%(net)s) is not "
"supported by the specified consistency group "
"(%(cg)s).") % params
raise exception.InvalidParameterValue(msg)
options = {'size': size,
'user_id': context.user_id,
'project_id': context.project_id,
@ -178,7 +222,10 @@ class API(base.Base):
'share_proto': share_proto,
'share_type_id': share_type_id,
'is_public': is_public,
'consistency_group_id': consistency_group_id,
}
if cgsnapshot_member:
options['source_cgsnapshot_member_id'] = cgsnapshot_member['id']
try:
share = self.db.share_create(context, options,
@ -198,12 +245,15 @@ class API(base.Base):
host = snapshot['share']['host']
self.create_instance(context, share, share_network_id=share_network_id,
host=host, availability_zone=availability_zone)
host=host, availability_zone=availability_zone,
consistency_group=consistency_group,
cgsnapshot_member=cgsnapshot_member)
return share
def create_instance(self, context, share, share_network_id=None,
host=None, availability_zone=None):
host=None, availability_zone=None,
consistency_group=None, cgsnapshot_member=None):
policy.check_policy(context, 'share', 'create')
availability_zone_id = None
@ -226,6 +276,14 @@ class API(base.Base):
}
)
if cgsnapshot_member:
host = cgsnapshot_member['share']['host']
share = self.db.share_instance_update(context,
share_instance['id'],
{'host': host})
# NOTE(ameade): Do not cast to driver if creating from cgsnapshot
return
share_dict = share.to_dict()
share_dict.update(
{'metadata': self.db.share_metadata_get(context, share['id'])}
@ -243,6 +301,7 @@ class API(base.Base):
'share_id': share['id'],
'snapshot_id': share['snapshot_id'],
'share_type': share_type,
'consistency_group': consistency_group,
}
if host:
@ -333,6 +392,13 @@ class API(base.Base):
msg = _("Share still has %d dependent snapshots") % len(snapshots)
raise exception.InvalidShare(reason=msg)
cgsnapshot_members_count = self.db.count_cgsnapshot_members_in_share(
context, share_id)
if cgsnapshot_members_count:
msg = (_("Share still has %d dependent cgsnapshot members") %
cgsnapshot_members_count)
raise exception.InvalidShare(reason=msg)
try:
reservations = QUOTAS.reserve(context,
project_id=project_id,

View File

@ -44,7 +44,16 @@ def stub_share(id, **kwargs):
'snapshot_support': True,
}
share.update(kwargs)
return share
# NOTE(ameade): We must wrap the dictionary in an class in order to stub
# object attributes.
class wrapper(dict):
pass
fake_share = wrapper()
fake_share.instance = {'id': "fake_instance_id"}
fake_share.update(share)
return fake_share
def stub_snapshot(id, **kwargs):
@ -124,3 +133,23 @@ def stub_snapshot_delete(self, context, *args, **param):
def stub_snapshot_get_all_by_project(self, context, search_opts=None,
sort_key=None, sort_dir=None):
return [stub_snapshot_get(self, context, 2)]
def stub_cgsnapshot_member(id, **kwargs):
member = {
'id': id,
'share_id': 'fakeshareid',
'share_instance_id': 'fakeshareinstanceid',
'share_proto': 'fakesnapproto',
'share_type_id': 'fake_share_type_id',
'export_location': 'fakesnaplocation',
'user_id': 'fakesnapuser',
'project_id': 'fakesnapproject',
'host': 'fakesnaphost',
'share_size': 1,
'size': 1,
'status': 'fakesnapstatus',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
}
member.update(kwargs)
return member

View File

@ -19,6 +19,7 @@ from oslo_serialization import jsonutils
import six
import webob
from manila.api.openstack import wsgi
from manila.common import constants
from manila import context
from manila import db
@ -95,6 +96,27 @@ class AdminActionsTest(test.TestCase):
'/v1/fake/share_instances/%s/action' % instance['id'])
return instance, req
def _setup_cg_data(self, cg=None):
if cg is None:
cg = db_utils.create_consistency_group(
status=constants.STATUS_AVAILABLE)
req = webob.Request.blank('/v1/fake/consistency-groups/%s/action' %
cg['id'])
req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '1.5'
req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True'
return cg, req
def _setup_cgsnapshot_data(self, cgsnapshot=None):
if cgsnapshot is None:
cgsnapshot = db_utils.create_cgsnapshot(
'fake_id', status=constants.STATUS_AVAILABLE)
req = webob.Request.blank('/v1/fake/cgsnapshots/%s/action' %
cgsnapshot['id'])
req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '1.5'
req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True'
return cgsnapshot, req
def _reset_status(self, ctxt, model, req, db_access_method,
valid_code, valid_status=None, body=None):
if body is None:
@ -149,6 +171,26 @@ class AdminActionsTest(test.TestCase):
self._reset_status(ctxt, instance, req, db.share_instance_get,
valid_code, valid_status)
@ddt.data(*fixture_reset_status_with_different_roles)
@ddt.unpack
def test_consistency_groups_reset_status_with_different_roles(
self, role, valid_code, valid_status):
ctxt = self._get_context(role)
cg, req = self._setup_cg_data()
self._reset_status(ctxt, cg, req, db.consistency_group_get,
valid_code, valid_status)
@ddt.data(*fixture_reset_status_with_different_roles)
@ddt.unpack
def test_cgsnapshot_reset_status_with_different_roles(
self, role, valid_code, valid_status):
ctxt = self._get_context(role)
cgsnap, req = self._setup_cgsnapshot_data()
self._reset_status(ctxt, cgsnap, req, db.cgsnapshot_get,
valid_code, valid_status)
@ddt.data(*fixture_invalid_reset_status_body)
def test_share_invalid_reset_status_body(self, body):
share, req = self._setup_share_data()
@ -244,3 +286,23 @@ class AdminActionsTest(test.TestCase):
ctxt = self._get_context('admin')
self._force_delete(ctxt, instance, req, db.share_instance_get, 404)
@ddt.data(*fixture_force_delete_with_different_roles)
@ddt.unpack
def test_consistency_group_force_delete_with_different_roles(self, role,
resp_code):
cg, req = self._setup_cg_data()
ctxt = self._get_context(role)
self._force_delete(ctxt, cg, req, db.consistency_group_get,
resp_code)
@ddt.data(*fixture_force_delete_with_different_roles)
@ddt.unpack
def test_cgsnapshot_force_delete_with_different_roles(self, role,
resp_code):
cgsnap, req = self._setup_cgsnapshot_data()
ctxt = self._get_context(role)
self._force_delete(ctxt, cgsnap, req, db.cgsnapshot_get,
resp_code)

View File

@ -111,12 +111,14 @@ class HTTPRequest(os_wsgi.Request):
kwargs['base_url'] = 'http://localhost/v1'
use_admin_context = kwargs.pop('use_admin_context', False)
version = kwargs.pop('version', api_version.DEFAULT_API_VERSION)
experimental = kwargs.pop('experimental', False)
out = os_wsgi.Request.blank(*args, **kwargs)
out.environ['manila.context'] = FakeRequestContext(
'fake_user',
'fake',
is_admin=use_admin_context)
out.api_version_request = api_version.APIVersionRequest(version)
out.api_version_request = api_version.APIVersionRequest(
version, experimental=experimental)
return out

View File

@ -0,0 +1,427 @@
# Copyright 2015 Alex Meade
# 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 copy
import datetime
import uuid
import mock
from oslo_config import cfg
import six
import webob
import manila.api.v1.cgsnapshots as cgs
from manila.common import constants
from manila import exception
from manila import test
from manila.tests.api import fakes
CONF = cfg.CONF
class CGSnapshotApiTest(test.TestCase):
def setUp(self):
super(CGSnapshotApiTest, self).setUp()
self.controller = cgs.CGSnapshotController()
self.api_version = '1.5'
self.request = fakes.HTTPRequest.blank('/consistency-groups',
version=self.api_version,
experimental=True)
def _get_fake_cgsnapshot(self, **values):
snap = {
'id': 'fake_id',
'user_id': 'fakeuser',
'project_id': 'fakeproject',
'status': constants.STATUS_CREATING,
'name': None,
'description': None,
'consistency_group_id': None,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
}
snap.update(**values)
expected_snap = copy.deepcopy(snap)
del expected_snap['user_id']
expected_snap['links'] = mock.ANY
return snap, expected_snap
def _get_fake_simple_cgsnapshot(self, **values):
snap = {
'id': 'fake_id',
'name': None,
}
snap.update(**values)
expected_snap = copy.deepcopy(snap)
expected_snap['links'] = mock.ANY
return snap, expected_snap
def _get_fake_cgsnapshot_member(self, **values):
member = {
'id': 'fake_id',
'user_id': 'fakeuser',
'project_id': 'fakeproject',
'status': constants.STATUS_CREATING,
'cgsnapshot_id': None,
'share_proto': None,
'share_type_id': None,
'share_id': None,
'size': None,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
}
member.update(**values)
expected_member = copy.deepcopy(member)
del expected_member['user_id']
del expected_member['status']
expected_member['share_protocol'] = member['share_proto']
del expected_member['share_proto']
return member, expected_member
def test_create_invalid_body(self):
body = {"not_cg_snapshot": {}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_create_no_consistency_group_id(self):
body = {"cgnapshot": {}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_create(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
fake_id = six.text_type(uuid.uuid4())
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
mock.Mock(return_value=fake_snap))
body = {"cgsnapshot": {"consistency_group_id": fake_id}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
context, consistency_group_id=fake_id)
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
def test_create_cg_does_not_exist(self):
fake_id = six.text_type(uuid.uuid4())
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
mock.Mock(
side_effect=exception.ConsistencyGroupNotFound(
consistency_group_id=six.text_type(
uuid.uuid4())
)))
body = {"cgsnapshot": {"consistency_group_id": fake_id}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_create_cg_does_not_a_uuid(self):
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
mock.Mock(
side_effect=exception.ConsistencyGroupNotFound(
consistency_group_id='not_a_uuid'
)))
body = {"cgsnapshot": {"consistency_group_id": "not_a_uuid"}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_create_invalid_cg(self):
fake_id = six.text_type(uuid.uuid4())
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
mock.Mock(
side_effect=exception.InvalidConsistencyGroup(
reason='bad_status'
)))
body = {"cgsnapshot": {"consistency_group_id": fake_id}}
self.assertRaises(webob.exc.HTTPConflict, self.controller.create,
self.request, body)
def test_create_with_name(self):
fake_name = 'fake_name'
fake_snap, expected_snap = self._get_fake_cgsnapshot(name=fake_name)
fake_id = six.text_type(uuid.uuid4())
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
mock.Mock(return_value=fake_snap))
body = {"cgsnapshot": {"consistency_group_id": fake_id,
"name": fake_name}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
context, consistency_group_id=fake_id, name=fake_name)
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
def test_create_with_description(self):
fake_description = 'fake_description'
fake_snap, expected_snap = self._get_fake_cgsnapshot(
description=fake_description)
fake_id = six.text_type(uuid.uuid4())
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
mock.Mock(return_value=fake_snap))
body = {"cgsnapshot": {"consistency_group_id": fake_id,
"description": fake_description}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
context, consistency_group_id=fake_id,
description=fake_description)
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
def test_create_with_name_and_description(self):
fake_name = 'fake_name'
fake_description = 'fake_description'
fake_id = six.text_type(uuid.uuid4())
fake_snap, expected_snap = self._get_fake_cgsnapshot(
description=fake_description, name=fake_name)
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
mock.Mock(return_value=fake_snap))
body = {"cgsnapshot": {"consistency_group_id": fake_id,
"description": fake_description,
"name": fake_name}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
context, consistency_group_id=fake_id, name=fake_name,
description=fake_description)
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
def test_update_with_name_and_description(self):
fake_name = 'fake_name'
fake_description = 'fake_description'
fake_id = six.text_type(uuid.uuid4())
fake_snap, expected_snap = self._get_fake_cgsnapshot(
description=fake_description, name=fake_name)
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
mock.Mock(return_value=fake_snap))
self.mock_object(self.controller.cg_api, 'update_cgsnapshot',
mock.Mock(return_value=fake_snap))
body = {"cgsnapshot": {"description": fake_description,
"name": fake_name}}
context = self.request.environ['manila.context']
res_dict = self.controller.update(self.request, fake_id, body)
self.controller.cg_api.update_cgsnapshot.assert_called_once_with(
context, fake_snap,
dict(name=fake_name, description=fake_description))
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
def test_update_snapshot_not_found(self):
body = {"cgsnapshot": {}}
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.update,
self.request, 'fake_id', body)
def test_update_invalid_body(self):
body = {"not_cgsnapshot": {}}
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.request, 'fake_id', body)
def test_update_invalid_body_invalid_field(self):
body = {"cgsnapshot": {"unknown_field": ""}}
exc = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.request, 'fake_id', body)
self.assertTrue('unknown_field' in six.text_type(exc))
def test_update_invalid_body_readonly_field(self):
body = {"cgsnapshot": {"created_at": []}}
exc = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.request, 'fake_id', body)
self.assertTrue('created_at' in six.text_type(exc))
def test_list_index(self):
fake_snap, expected_snap = self._get_fake_simple_cgsnapshot()
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[fake_snap]))
res_dict = self.controller.index(self.request)
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
def test_list_index_no_cgs(self):
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[]))
res_dict = self.controller.index(self.request)
self.assertEqual([], res_dict['cgsnapshots'])
def test_list_index_with_limit(self):
fake_snap, expected_snap = self._get_fake_simple_cgsnapshot()
fake_snap2, expected_snap2 = self._get_fake_simple_cgsnapshot(
id="fake_id2")
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[fake_snap, fake_snap2]))
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.index(req)
self.assertEqual(1, len(res_dict['cgsnapshots']))
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
def test_list_index_with_limit_and_offset(self):
fake_snap, expected_snap = self._get_fake_simple_cgsnapshot()
fake_snap2, expected_snap2 = self._get_fake_simple_cgsnapshot(
id="fake_id2")
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[fake_snap, fake_snap2]))
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1&offset=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.index(req)
self.assertEqual(1, len(res_dict['cgsnapshots']))
self.assertEqual([expected_snap2], res_dict['cgsnapshots'])
def test_list_detail(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[fake_snap]))
res_dict = self.controller.detail(self.request)
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
def test_list_detail_no_cgs(self):
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[]))
res_dict = self.controller.detail(self.request)
self.assertEqual([], res_dict['cgsnapshots'])
def test_list_detail_with_limit(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
fake_snap2, expected_snap2 = self._get_fake_cgsnapshot(
id="fake_id2")
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[fake_snap, fake_snap2]))
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['cgsnapshots']))
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
def test_list_detail_with_limit_and_offset(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
fake_snap2, expected_snap2 = self._get_fake_cgsnapshot(
id="fake_id2")
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
mock.Mock(return_value=[fake_snap, fake_snap2]))
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1&offset=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['cgsnapshots']))
self.assertEqual([expected_snap2], res_dict['cgsnapshots'])
def test_delete(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
mock.Mock(return_value=fake_snap))
self.mock_object(self.controller.cg_api, 'delete_cgsnapshot')
res = self.controller.delete(self.request, fake_snap['id'])
self.assertEqual(202, res.status_code)
def test_delete_not_found(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
self.request, fake_snap['id'])
def test_delete_in_conflicting_status(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
mock.Mock(return_value=fake_snap))
self.mock_object(self.controller.cg_api, 'delete_cgsnapshot',
mock.Mock(
side_effect=exception.InvalidCGSnapshot(
reason='blah')))
self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
self.request, fake_snap['id'])
def test_show(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
mock.Mock(return_value=fake_snap))
res_dict = self.controller.show(self.request, fake_snap['id'])
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
def test_show_cg_not_found(self):
fake_snap, expected_snap = self._get_fake_cgsnapshot()
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
self.request, fake_snap['id'])
def test_members_empty(self):
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
mock.Mock(return_value=[]))
res_dict = self.controller.members(self.request, 'fake_cg_id')
self.assertEqual([], res_dict['cgsnapshot_members'])
def test_members(self):
fake_member, expected_member = self._get_fake_cgsnapshot_member()
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
mock.Mock(return_value=[fake_member]))
res_dict = self.controller.members(self.request, 'fake_cg_id')
self.assertEqual([expected_member], res_dict['cgsnapshot_members'])
def test_members_with_limit(self):
fake_member, expected_member = self._get_fake_cgsnapshot_member()
fake_member2, expected_member2 = self._get_fake_cgsnapshot_member(
id="fake_id2")
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
mock.Mock(return_value=[fake_member, fake_member2]))
req = fakes.HTTPRequest.blank('/members?limit=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.members(req, 'fake_cg_id')
self.assertEqual(1, len(res_dict['cgsnapshot_members']))
def test_members_with_limit_and_offset(self):
fake_member, expected_member = self._get_fake_cgsnapshot_member()
fake_member2, expected_member2 = self._get_fake_cgsnapshot_member(
id="fake_id2")
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
mock.Mock(return_value=[fake_member, fake_member2]))
req = fakes.HTTPRequest.blank('/members?limit=1&offset=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.members(req, 'fake_cg_id')
self.assertEqual(1, len(res_dict['cgsnapshot_members']))
self.assertEqual([expected_member2], res_dict['cgsnapshot_members'])

View File

@ -0,0 +1,505 @@
# Copyright 2015 Alex Meade
# 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 copy
import datetime
import uuid
import mock
from oslo_config import cfg
import six
import webob
import manila.api.v1.consistency_groups as cgs
from manila.common import constants
import manila.consistency_group.api as cg_api
from manila import exception
from manila.share import share_types
from manila import test
from manila.tests.api import fakes
CONF = cfg.CONF
class CGApiTest(test.TestCase):
"""Share Api Test."""
def setUp(self):
super(CGApiTest, self).setUp()
self.controller = cgs.CGController()
self.fake_share_type = {'id': six.text_type(uuid.uuid4())}
self.api_version = '1.5'
self.request = fakes.HTTPRequest.blank('/consistency-groups',
version=self.api_version,
experimental=True)
def _get_fake_cg(self, **values):
cg = {
'id': 'fake_id',
'user_id': 'fakeuser',
'project_id': 'fakeproject',
'status': constants.STATUS_CREATING,
'name': None,
'description': None,
'host': None,
'source_cgsnapshot_id': None,
'share_network_id': None,
'share_server_id': None,
'share_types': [],
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
}
cg.update(**values)
expected_cg = copy.deepcopy(cg)
del expected_cg['user_id']
del expected_cg['share_server_id']
expected_cg['links'] = mock.ANY
expected_cg['share_types'] = [st['share_type_id']
for st in cg.get('share_types')]
return cg, expected_cg
def _get_fake_simple_cg(self, **values):
cg = {
'id': 'fake_id',
'name': None,
}
cg.update(**values)
expected_cg = copy.deepcopy(cg)
expected_cg['links'] = mock.ANY
return cg, expected_cg
def test_cg_create(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=self.fake_share_type))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, share_type_ids=[self.fake_share_type['id']])
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_invalid_cgsnapshot_state(self):
fake_snap_id = six.text_type(uuid.uuid4())
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(side_effect=exception.InvalidCGSnapshot(
reason='bad status'
)))
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
self.assertRaises(webob.exc.HTTPConflict,
self.controller.create, self.request, body)
def test_cg_create_no_default_share_type(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=None))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_with_name(self):
fake_name = 'fake_name'
fake_cg, expected_cg = self._get_fake_cg(name=fake_name)
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=self.fake_share_type))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {"name": fake_name}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, name=fake_name,
share_type_ids=[self.fake_share_type['id']])
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_with_description(self):
fake_description = 'fake_description'
fake_cg, expected_cg = self._get_fake_cg(description=fake_description)
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=self.fake_share_type))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {"description": fake_description}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, description=fake_description,
share_type_ids=[self.fake_share_type['id']])
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_with_share_types(self):
fake_share_types = [{"share_type_id": self.fake_share_type['id']}]
fake_cg, expected_cg = self._get_fake_cg(share_types=fake_share_types)
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {
"share_types": [self.fake_share_type['id']]}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, share_type_ids=[self.fake_share_type['id']])
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_with_source_cgsnapshot_id(self):
fake_snap_id = six.text_type(uuid.uuid4())
fake_cg, expected_cg = self._get_fake_cg(
source_cgsnapshot_id=fake_snap_id)
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=self.fake_share_type))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {
"source_cgsnapshot_id": fake_snap_id}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, source_cgsnapshot_id=fake_snap_id)
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_with_share_network_id(self):
fake_net_id = six.text_type(uuid.uuid4())
fake_cg, expected_cg = self._get_fake_cg(
share_network_id=fake_net_id)
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=self.fake_share_type))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {
"share_network_id": fake_net_id}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, share_network_id=fake_net_id, share_type_ids=mock.ANY)
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_no_default_share_type_with_cgsnapshot(self):
fake_snap_id = six.text_type(uuid.uuid4())
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=None))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {
"source_cgsnapshot_id": fake_snap_id}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, source_cgsnapshot_id=fake_snap_id)
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_with_name_and_description(self):
fake_name = 'fake_name'
fake_description = 'fake_description'
fake_cg, expected_cg = self._get_fake_cg(name=fake_name,
description=fake_description)
self.mock_object(share_types, 'get_default_share_type',
mock.Mock(return_value=self.fake_share_type))
self.mock_object(self.controller.cg_api, 'create',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {"name": fake_name,
"description": fake_description}}
context = self.request.environ['manila.context']
res_dict = self.controller.create(self.request, body)
self.controller.cg_api.create.assert_called_once_with(
context, name=fake_name, description=fake_description,
share_type_ids=[self.fake_share_type['id']])
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_create_invalid_body(self):
body = {"not_consistency_group": {}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_invalid_body_share_types_and_source_cgsnapshot(self):
body = {"consistency_group": {"share_types": [],
"source_cgsnapshot_id": ""}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_source_cgsnapshot_not_in_available(self):
fake_snap_id = six.text_type(uuid.uuid4())
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
self.mock_object(self.controller.cg_api, 'create', mock.Mock(
side_effect=exception.InvalidCGSnapshot(reason='blah')))
self.assertRaises(webob.exc.HTTPConflict, self.controller.create,
self.request, body)
def test_cg_create_source_cgsnapshot_does_not_exist(self):
fake_snap_id = six.text_type(uuid.uuid4())
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
self.mock_object(self.controller.cg_api, 'create', mock.Mock(
side_effect=exception.CGSnapshotNotFound(
cgsnapshot_id=fake_snap_id)))
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_source_cgsnapshot_not_a_uuid(self):
fake_snap_id = "Not a uuid"
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_share_network_id_not_a_uuid(self):
fake_net_id = "Not a uuid"
body = {"consistency_group": {"share_network_id": fake_net_id}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_invalid_body_share_types_not_a_list(self):
body = {"consistency_group": {"share_types": ""}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_invalid_body_invalid_field(self):
body = {"consistency_group": {"unknown_field": ""}}
exc = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
self.request, body)
self.assertTrue('unknown_field' in six.text_type(exc))
def test_cg_create_with_invalid_share_types_field(self):
body = {"consistency_group": {"share_types": 'iamastring'}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_create_with_invalid_share_types_field_not_uuids(self):
body = {"consistency_group": {"share_types": ['iamastring']}}
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.request, body)
def test_cg_update_with_name_and_description(self):
fake_name = 'fake_name'
fake_description = 'fake_description'
fake_cg, expected_cg = self._get_fake_cg(name=fake_name,
description=fake_description)
self.mock_object(self.controller.cg_api, 'get',
mock.Mock(return_value=fake_cg))
self.mock_object(self.controller.cg_api, 'update',
mock.Mock(return_value=fake_cg))
body = {"consistency_group": {"name": fake_name,
"description": fake_description}}
context = self.request.environ['manila.context']
res_dict = self.controller.update(self.request, fake_cg['id'], body)
self.controller.cg_api.update.assert_called_once_with(
context, fake_cg,
{"name": fake_name, "description": fake_description})
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_update_cg_not_found(self):
body = {"consistency_group": {}}
self.mock_object(self.controller.cg_api, 'get',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.update,
self.request, 'fake_id', body)
def test_cg_update_invalid_body(self):
body = {"not_consistency_group": {}}
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.request, 'fake_id', body)
def test_cg_update_invalid_body_invalid_field(self):
body = {"consistency_group": {"unknown_field": ""}}
exc = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.request, 'fake_id', body)
self.assertTrue('unknown_field' in six.text_type(exc))
def test_cg_update_invalid_body_readonly_field(self):
body = {"consistency_group": {"share_types": []}}
exc = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update,
self.request, 'fake_id', body)
self.assertTrue('share_types' in six.text_type(exc))
def test_cg_list_index(self):
fake_cg, expected_cg = self._get_fake_simple_cg()
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[fake_cg]))
res_dict = self.controller.index(self.request)
self.assertEqual([expected_cg], res_dict['consistency_groups'])
def test_cg_list_index_no_cgs(self):
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[]))
res_dict = self.controller.index(self.request)
self.assertEqual([], res_dict['consistency_groups'])
def test_cg_list_index_with_limit(self):
fake_cg, expected_cg = self._get_fake_simple_cg()
fake_cg2, expected_cg2 = self._get_fake_simple_cg(id="fake_id2")
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[fake_cg, fake_cg2]))
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.index(req)
self.assertEqual(1, len(res_dict['consistency_groups']))
self.assertEqual([expected_cg], res_dict['consistency_groups'])
def test_cg_list_index_with_limit_and_offset(self):
fake_cg, expected_cg = self._get_fake_simple_cg()
fake_cg2, expected_cg2 = self._get_fake_simple_cg(id="fake_id2")
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[fake_cg, fake_cg2]))
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1&offset=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.index(req)
self.assertEqual(1, len(res_dict['consistency_groups']))
self.assertEqual([expected_cg2], res_dict['consistency_groups'])
def test_cg_list_detail(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[fake_cg]))
res_dict = self.controller.detail(self.request)
self.assertEqual([expected_cg], res_dict['consistency_groups'])
def test_cg_list_detail_no_cgs(self):
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[]))
res_dict = self.controller.detail(self.request)
self.assertEqual([], res_dict['consistency_groups'])
def test_cg_list_detail_with_limit(self):
fake_cg, expected_cg = self._get_fake_cg()
fake_cg2, expected_cg2 = self._get_fake_cg(id="fake_id2")
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[fake_cg, fake_cg2]))
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['consistency_groups']))
self.assertEqual([expected_cg], res_dict['consistency_groups'])
def test_cg_list_detail_with_limit_and_offset(self):
fake_cg, expected_cg = self._get_fake_cg()
fake_cg2, expected_cg2 = self._get_fake_cg(id="fake_id2")
self.mock_object(cg_api.API, 'get_all',
mock.Mock(return_value=[fake_cg, fake_cg2]))
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1&offset=1',
version=self.api_version,
experimental=True)
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['consistency_groups']))
self.assertEqual([expected_cg2], res_dict['consistency_groups'])
def test_cg_delete(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(cg_api.API, 'get',
mock.Mock(return_value=fake_cg))
self.mock_object(cg_api.API, 'delete')
res = self.controller.delete(self.request, fake_cg['id'])
self.assertEqual(202, res.status_code)
def test_cg_delete_cg_not_found(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(cg_api.API, 'get',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
self.request, fake_cg['id'])
def test_cg_delete_in_conflicting_status(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(cg_api.API, 'get',
mock.Mock(return_value=fake_cg))
self.mock_object(cg_api.API, 'delete', mock.Mock(
side_effect=exception.InvalidConsistencyGroup(reason='blah')))
self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
self.request, fake_cg['id'])
def test_cg_show(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(cg_api.API, 'get',
mock.Mock(return_value=fake_cg))
req = fakes.HTTPRequest.blank(
'/consistency_groups/%s' % fake_cg['id'],
version=self.api_version, experimental=True)
res_dict = self.controller.show(req, fake_cg['id'])
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_show_as_admin(self):
fake_cg, expected_cg = self._get_fake_cg()
expected_cg['share_server_id'] = None
self.mock_object(cg_api.API, 'get',
mock.Mock(return_value=fake_cg))
req = fakes.HTTPRequest.blank(
'/consistency_groups/%s' % fake_cg['id'],
version=self.api_version, experimental=True)
admin_context = req.environ['manila.context'].elevated()
req.environ['manila.context'] = admin_context
res_dict = self.controller.show(req, fake_cg['id'])
self.assertEqual(expected_cg, res_dict['consistency_group'])
def test_cg_show_cg_not_found(self):
fake_cg, expected_cg = self._get_fake_cg()
self.mock_object(cg_api.API, 'get',
mock.Mock(side_effect=exception.NotFound))
req = fakes.HTTPRequest.blank(
'/consistency_groups/%s' % fake_cg['id'],
version=self.api_version, experimental=True)
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
req, fake_cg['id'])

View File

@ -265,7 +265,7 @@ class ShareNetworkAPITest(test.TestCase):
self.assertTrue(share_networks.QUOTAS.reserve.called)
self.assertFalse(share_networks.QUOTAS.commit.called)
def test_delete_in_use(self):
def test_delete_in_use_by_share(self):
share_nw = fake_share_network.copy()
self.mock_object(db_api, 'share_network_get',
mock.Mock(return_value=share_nw))
@ -283,6 +283,21 @@ class ShareNetworkAPITest(test.TestCase):
assert_called_once_with(self.req.environ['manila.context'],
share_nw['id'])
def test_delete_in_use_by_consistency_group(self):
share_nw = fake_share_network.copy()
self.mock_object(db_api, 'share_network_get',
mock.Mock(return_value=share_nw))
self.mock_object(db_api, 'count_consistency_groups_in_share_network',
mock.Mock(return_value=2))
self.assertRaises(webob_exc.HTTPConflict,
self.controller.delete,
self.req,
share_nw['id'])
db_api.share_network_get.assert_called_once_with(
self.req.environ['manila.context'], share_nw['id'])
def test_show_nominal(self):
share_nw = 'fake network id'
with mock.patch.object(db_api,

View File

@ -127,6 +127,19 @@ class ShareApiTest(test.TestCase):
expected = self._get_expected_share_detailed_response(self.share)
self.assertEqual(expected, res_dict)
def test_share_create_with_consistency_group(self):
self.mock_object(share_api.API, 'create', self.create_mock)
body = {"share": copy.deepcopy(self.share)}
req = fakes.HTTPRequest.blank('/shares', version="1.5")
res_dict = self.controller.create(req, body)
expected = self._get_expected_share_detailed_response(self.share)
expected['share']['consistency_group_id'] = None
expected['share']['source_cgsnapshot_member_id'] = None
self.assertEqual(expected, res_dict)
def test_share_create_with_valid_default_share_type(self):
self.mock_object(share_types, 'get_share_type_by_name',
mock.Mock(return_value=self.vt))
@ -337,6 +350,14 @@ class ShareApiTest(test.TestCase):
expected = self._get_expected_share_detailed_response()
self.assertEqual(expected, res_dict)
def test_share_show_with_consistency_group(self):
req = fakes.HTTPRequest.blank('/shares/1', version='1.5')
res_dict = self.controller.show(req, '1')
expected = self._get_expected_share_detailed_response()
expected['share']['consistency_group_id'] = None
expected['share']['source_cgsnapshot_member_id'] = None
self.assertEqual(expected, res_dict)
def test_share_show_admin(self):
req = fakes.HTTPRequest.blank('/shares/1', use_admin_context=True)
res_dict = self.controller.show(req, '1')
@ -356,6 +377,35 @@ class ShareApiTest(test.TestCase):
resp = self.controller.delete(req, 1)
self.assertEqual(resp.status_int, 202)
def test_share_delete_in_consistency_group_param_not_provided(self):
fake_share = stubs.stub_share('fake_share',
consistency_group_id='fake_cg_id')
self.mock_object(share_api.API, 'get',
mock.Mock(return_value=fake_share))
req = fakes.HTTPRequest.blank('/shares/1')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete, req, 1)
def test_share_delete_in_consistency_group(self):
fake_share = stubs.stub_share('fake_share',
consistency_group_id='fake_cg_id')
self.mock_object(share_api.API, 'get',
mock.Mock(return_value=fake_share))
req = fakes.HTTPRequest.blank(
'/shares/1?consistency_group_id=fake_cg_id')
resp = self.controller.delete(req, 1)
self.assertEqual(resp.status_int, 202)
def test_share_delete_in_consistency_group_wrong_id(self):
fake_share = stubs.stub_share('fake_share',
consistency_group_id='fake_cg_id')
self.mock_object(share_api.API, 'get',
mock.Mock(return_value=fake_share))
req = fakes.HTTPRequest.blank(
'/shares/1?consistency_group_id=not_fake_cg_id')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.delete, req, 1)
def test_share_update(self):
shr = self.share
body = {"share": shr}
@ -368,6 +418,15 @@ class ShareApiTest(test.TestCase):
self.assertEqual(shr['is_public'],
res_dict['share']['is_public'])
def test_share_update_with_consistency_group(self):
shr = self.share
body = {"share": shr}
req = fakes.HTTPRequest.blank('/share/1', version="1.5")
res_dict = self.controller.update(req, 1, body)
self.assertIsNone(res_dict['share']["consistency_group_id"])
self.assertIsNone(res_dict['share']["source_cgsnapshot_member_id"])
def test_share_not_updates_size(self):
req = fakes.HTTPRequest.blank('/share/1')
res_dict = self.controller.update(req, 1, {"share": self.share})
@ -600,6 +659,53 @@ class ShareApiTest(test.TestCase):
self.assertEqual(res_dict['shares'][0]['volume_type'],
res_dict['shares'][0]['share_type'])
def test_share_list_detail_with_consistency_group(self):
self.mock_object(share_api.API, 'get_all',
stubs.stub_share_get_all_by_project)
env = {'QUERY_STRING': 'name=Share+Test+Name'}
req = fakes.HTTPRequest.blank('/shares/detail', environ=env,
version="1.5")
res_dict = self.controller.detail(req)
expected = {
'shares': [
{
'status': 'fakestatus',
'description': 'displaydesc',
'export_location': 'fake_location',
'export_locations': ['fake_location', 'fake_location2'],
'availability_zone': 'fakeaz',
'name': 'displayname',
'share_proto': 'FAKEPROTO',
'metadata': {},
'project_id': 'fakeproject',
'host': 'fakehost',
'id': '1',
'snapshot_id': '2',
'share_network_id': None,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'size': 1,
'share_type': '1',
'volume_type': '1',
'is_public': False,
'consistency_group_id': None,
'source_cgsnapshot_member_id': None,
'links': [
{
'href': 'http://localhost/v1/fake/shares/1',
'rel': 'self'
},
{
'href': 'http://localhost/fake/shares/1',
'rel': 'bookmark'
}
],
}
]
}
self.assertEqual(expected, res_dict)
self.assertEqual(res_dict['shares'][0]['volume_type'],
res_dict['shares'][0]['share_type'])
def test_remove_invalid_options(self):
ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False)
search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}

File diff suppressed because it is too large Load Diff

View File

@ -89,3 +89,9 @@ class FakeShareDriver(driver.ShareDriver):
def _verify_share_server_handling(self, driver_handles_share_servers):
return super(FakeShareDriver, self)._verify_share_server_handling(
driver_handles_share_servers)
def create_consistency_group(self, context, cg_id):
pass
def delete_consistency_group(self, context, cg_id):
pass

View File

@ -43,6 +43,10 @@
"share_extension:snapshot_admin_actions:reset_status": "rule:admin_api",
"share_extension:share_instance_admin_actions:force_delete": "rule:admin_api",
"share_extension:share_instance_admin_actions:reset_status": "rule:admin_api",
"share_extension:consistency_group_admin_actions:force_delete": "rule:admin_api",
"share_extension:consistency_group_admin_actions:reset_status": "rule:admin_api",
"share_extension:cgsnapshot_admin_actions:force_delete": "rule:admin_api",
"share_extension:cgsnapshot_admin_actions:reset_status": "rule:admin_api",
"share_extension:types_manage": "",
"share_extension:types_extra_specs": "",
"share_extension:share_type_access": "",
@ -58,5 +62,16 @@
"limits_extension:used_limits": "",
"scheduler_stats:pools:index": "rule:admin_api",
"scheduler_stats:pools:detail": "rule:admin_api"
"scheduler_stats:pools:detail": "rule:admin_api",
"consistency_group:create" : "rule:default",
"consistency_group:delete": "rule:default",
"consistency_group:update": "rule:default",
"consistency_group:get": "rule:default",
"consistency_group:get_all": "rule:default",
"consistency_group:create_cgsnapshot" : "rule:default",
"consistency_group:delete_cgsnapshot": "rule:default",
"consistency_group:get_cgsnapshot": "rule:default",
"consistency_group:get_all_cgsnapshots": "rule:default"
}

View File

@ -40,6 +40,74 @@ from manila import utils
CONF = cfg.CONF
def fake_share(id, **kwargs):
share = {
'id': id,
'size': 1,
'user_id': 'fakeuser',
'project_id': 'fakeproject',
'snapshot_id': None,
'share_network_id': None,
'share_type_id': None,
'availability_zone': 'fakeaz',
'status': 'fakestatus',
'display_name': 'fakename',
'metadata': None,
'display_description': 'fakedesc',
'share_proto': 'nfs',
'export_location': 'fake_location',
'host': 'fakehost',
'is_public': False,
'consistency_group_id': None,
'scheduled_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'launched_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'terminated_at': datetime.datetime(1, 1, 1, 1, 1, 1)
}
share.update(kwargs)
return share
def fake_snapshot(id, **kwargs):
snapshot = {
'id': id,
'share_size': 1,
'size': 1,
'user_id': 'fakeuser',
'project_id': 'fakeproject',
'share_id': None,
'availability_zone': 'fakeaz',
'status': 'fakestatus',
'display_name': 'fakename',
'display_description': 'fakedesc',
'share_proto': 'nfs',
'progress': 'fakeprogress99%',
'scheduled_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'launched_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'terminated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'share': {'host': 'fake_source_host'},
}
snapshot.update(kwargs)
return snapshot
def fake_access(id, **kwargs):
access = {
'id': id,
'share_id': 'fakeshareid',
'access_type': 'fakeacctype',
'access_to': 'fakeaccto',
'access_level': 'rw',
'state': 'fakeactive',
'STATE_NEW': 'fakenew',
'STATE_ACTIVE': 'fakeactive',
'STATE_DELETING': 'fakedeleting',
'STATE_DELETED': 'fakedeleted',
'STATE_ERROR': 'fakeerror',
}
access.update(kwargs)
return access
_FAKE_LIST_OF_ALL_SHARES = [
{
'name': 'foo',
@ -883,7 +951,8 @@ class ShareAPITestCase(test.TestCase):
self.api.create_instance.assert_called_once_with(
self.context, share, share_network_id=share['share_network_id'],
host=valid_host,
availability_zone=snapshot['share']['availability_zone'])
availability_zone=snapshot['share']['availability_zone'],
consistency_group=None, cgsnapshot_member=None)
share_api.policy.check_policy.assert_called_once_with(
self.context, 'share', 'create')
quota.QUOTAS.reserve.assert_called_once_with(
@ -957,7 +1026,18 @@ class ShareAPITestCase(test.TestCase):
utils.IsAMatcher(context.RequestContext), share['id'])
def test_delete_wrong_status(self):
share = db_utils.create_share()
share = fake_share('fakeid')
self.mock_object(db_api, 'share_get', mock.Mock(return_value=share))
self.assertRaises(exception.InvalidShare, self.api.delete,
self.context, share)
@mock.patch.object(db_api, 'count_cgsnapshot_members_in_share',
mock.Mock(return_value=2))
def test_delete_dependent_cgsnapshot_members(self):
share_server_id = 'fake-ss-id'
share = self._setup_delete_mocks(constants.STATUS_AVAILABLE,
share_server_id)
self.assertRaises(exception.InvalidShare, self.api.delete,
self.context, share)

View File

@ -36,7 +36,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion",
default="1.4",
default="1.5",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
cfg.StrOpt("region",