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: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:force_delete": "rule:admin_api",
"share_extension:share_instance_admin_actions:reset_status": "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:services": "rule:admin_api",
"share_extension:availability_zones": "", "share_extension:availability_zones": "",
@ -78,5 +82,16 @@
"share_network:get_all_share_networks": "rule:admin_api", "share_network:get_all_share_networks": "rule:admin_api",
"scheduler_stats:pools:index": "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 from six.moves.urllib import parse
import webob import webob
from manila.api.openstack import api_version_request as api_version
from manila.api.openstack import versioned_method
from manila.i18n import _ from manila.i18n import _
api_common_opts = [ api_common_opts = [
@ -201,6 +203,7 @@ class ViewBuilder(object):
"""Model API responses as dictionaries.""" """Model API responses as dictionaries."""
_collection_name = None _collection_name = None
_detail_version_modifiers = []
def _get_links(self, request, identifier): def _get_links(self, request, identifier):
return [{"rel": "self", return [{"rel": "self",
@ -262,6 +265,43 @@ class ViewBuilder(object):
url_parts[0:2] = prefix_parts[0:2] url_parts[0:2] = prefix_parts[0:2]
return parse.urlunsplit(url_parts) 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): def remove_invalid_options(context, search_options, allowed_search_options):
"""Remove search options that are not valid for non-admin API/context.""" """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 import extensions
from manila.api.openstack import wsgi from manila.api.openstack import wsgi
from manila.common import constants from manila.common import constants
import manila.consistency_group.api as cg_api
from manila import db from manila import db
from manila import exception from manila import exception
from manila import share from manila import share
@ -42,8 +43,9 @@ class AdminController(wsgi.Controller):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AdminController, self).__init__(*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.share_api = share.API()
self.cg_api = cg_api.API()
def _update(self, *args, **kwargs): def _update(self, *args, **kwargs):
raise NotImplementedError() raise NotImplementedError()
@ -143,18 +145,80 @@ class SnapshotAdminController(AdminController):
return self.share_api.delete_snapshot(*args, **kwargs) 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): class Admin_actions(extensions.ExtensionDescriptor):
"""Enable admin actions.""" """Enable admin actions."""
name = "AdminActions" name = "AdminActions"
alias = "os-admin-actions" 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): def get_controller_extensions(self):
exts = [] exts = []
controllers = (ShareAdminController, SnapshotAdminController, for class_ in (ShareAdminController, SnapshotAdminController,
ShareInstancesAdminController) ShareInstancesAdminController,
for class_ in controllers: CGAdminController, CGSnapshotAdminController):
controller = class_() controller = class_()
extension = extensions.ControllerExtension( extension = extensions.ControllerExtension(
self, class_.collection, controller) 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.2 - Share create() doesn't ignore availability_zone field of share.
* 1.3 - Snapshots become optional feature. * 1.3 - Snapshots become optional feature.
* 1.4 - Share instances admin API * 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 default api version request is defined to be the
# the minimum version of the API supported. # the minimum version of the API supported.
_MIN_API_VERSION = "1.0" _MIN_API_VERSION = "1.0"
_MAX_API_VERSION = "1.4" _MAX_API_VERSION = "1.5"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
@ -79,11 +80,11 @@ class APIVersionRequest(utils.ComparableMixin):
API microversions. API microversions.
""" """
def __init__(self, version_string=None): def __init__(self, version_string=None, experimental=False):
"""Create an API version request object.""" """Create an API version request object."""
self._ver_major = None self._ver_major = None
self._ver_minor = None self._ver_minor = None
self._experimental = False self._experimental = experimental
if version_string is not None: if version_string is not None:
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$", match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",

View File

@ -44,3 +44,9 @@ user documentation.
1.4 1.4
--- ---
Share instances admin API and update of Admin Actions extension. 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

@ -46,4 +46,4 @@ class VersionedMethod(utils.ComparableMixin):
def _cmpkey(self): def _cmpkey(self):
"""Return the value used by ComparableMixin for rich comparisons.""" """Return the value used by ComparableMixin for rich comparisons."""
return self.start_version return self.start_version

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 from manila.api import extensions
import manila.api.openstack 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 limits
from manila.api.v1 import scheduler_stats from manila.api.v1 import scheduler_stats
from manila.api.v1 import security_service from manila.api.v1 import security_service
@ -134,3 +136,20 @@ class APIRouter(manila.api.openstack.APIRouter):
controller=self.resources['scheduler_stats'], controller=self.resources['scheduler_stats'],
action='pools_detail', action='pools_detail',
conditions={'method': ['GET']}) 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)} 'len': len(share_instances)}
LOG.error(msg) LOG.error(msg)
raise exc.HTTPConflict(explanation=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']: for share_server in share_network['share_servers']:
self.share_rpcapi.delete_share_server(context, share_server) self.share_rpcapi.delete_share_server(context, share_server)
db_api.share_network_delete(context, id) db_api.share_network_delete(context, id)

View File

@ -64,6 +64,22 @@ class ShareController(wsgi.Controller):
try: try:
share = self.share_api.get(context, id) 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) self.share_api.delete(context, share)
except exception.NotFound: except exception.NotFound:
raise exc.HTTPNotFound() raise exc.HTTPNotFound()
@ -133,6 +149,7 @@ class ShareController(wsgi.Controller):
'display_name', 'status', 'share_server_id', 'volume_type_id', 'display_name', 'status', 'share_server_id', 'volume_type_id',
'share_type_id', 'snapshot_id', 'host', 'share_network_id', 'share_type_id', 'snapshot_id', 'host', 'share_network_id',
'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir', 'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir',
'consistency_group_id', 'cgsnapshot_id'
) )
def update(self, req, id, body): def update(self, req, id, body):
@ -162,14 +179,18 @@ class ShareController(wsgi.Controller):
share.update(update_dict) share.update(update_dict)
return self._view_builder.detail(req, share) return self._view_builder.detail(req, share)
@wsgi.Controller.api_version("1.3") @wsgi.Controller.api_version("1.5")
def create(self, req, body): def create(self, req, body):
return self._create(req, body) return self._create(req, body)
@wsgi.Controller.api_version("1.0", "1.2") # noqa @wsgi.Controller.api_version("1.0", "1.4") # noqa
def create(self, req, body): 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 = self._create(req, body)
share.pop('snapshot_support', None)
return share return share
def _create(self, req, body): def _create(self, req, body):
@ -211,6 +232,7 @@ class ShareController(wsgi.Controller):
'availability_zone': availability_zone, 'availability_zone': availability_zone,
'metadata': share.get('metadata'), 'metadata': share.get('metadata'),
'is_public': share.get('is_public', False), 'is_public': share.get('is_public', False),
'consistency_group_id': share.get('consistency_group_id')
} }
snapshot_id = share.get('snapshot_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.""" """Model a server API response as a python dictionary."""
_collection_name = 'shares' _collection_name = 'shares'
_detail_version_modifiers = ["add_consistency_group_fields"]
def summary_list(self, request, shares): def summary_list(self, request, shares):
"""Show a list of shares without many details.""" """Show a list of shares without many details."""
@ -76,10 +77,20 @@ class ViewBuilder(common.ViewBuilder):
'is_public': share.get('is_public'), 'is_public': share.get('is_public'),
'export_locations': export_locations, 'export_locations': export_locations,
} }
self.update_versioned_resource_dict(request, share_dict, share)
if context.is_admin: if context.is_admin:
share_dict['share_server_id'] = share.get('share_server_id') share_dict['share_server_id'] = share.get('share_server_id')
return {'share': share_dict} 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): def _list_view(self, func, request, shares):
"""Provide a view for a list of shares.""" """Provide a view for a list of shares."""
shares_list = [func(request, share)['share'] for share in 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.") message = _("Policy doesn't allow %(action)s to be performed.")
class Conflict(ManilaException):
message = _("%(err)s")
code = 409
class Invalid(ManilaException): class Invalid(ManilaException):
message = _("Unacceptable parameters.") message = _("Unacceptable parameters.")
code = 400 code = 400

View File

@ -66,7 +66,8 @@ class API(base.Base):
def create(self, context, share_proto, size, name, description, def create(self, context, share_proto, size, name, description,
snapshot=None, availability_zone=None, metadata=None, 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.""" """Create new share."""
policy.check_policy(context, 'share', 'create') policy.check_policy(context, 'share', 'create')
@ -167,6 +168,49 @@ class API(base.Base):
except ValueError as e: except ValueError as e:
raise exception.InvalidParameterValue(six.text_type(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, options = {'size': size,
'user_id': context.user_id, 'user_id': context.user_id,
'project_id': context.project_id, 'project_id': context.project_id,
@ -178,7 +222,10 @@ class API(base.Base):
'share_proto': share_proto, 'share_proto': share_proto,
'share_type_id': share_type_id, 'share_type_id': share_type_id,
'is_public': is_public, 'is_public': is_public,
'consistency_group_id': consistency_group_id,
} }
if cgsnapshot_member:
options['source_cgsnapshot_member_id'] = cgsnapshot_member['id']
try: try:
share = self.db.share_create(context, options, share = self.db.share_create(context, options,
@ -198,12 +245,15 @@ class API(base.Base):
host = snapshot['share']['host'] host = snapshot['share']['host']
self.create_instance(context, share, share_network_id=share_network_id, 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 return share
def create_instance(self, context, share, share_network_id=None, 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') policy.check_policy(context, 'share', 'create')
availability_zone_id = None 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 = share.to_dict()
share_dict.update( share_dict.update(
{'metadata': self.db.share_metadata_get(context, share['id'])} {'metadata': self.db.share_metadata_get(context, share['id'])}
@ -243,6 +301,7 @@ class API(base.Base):
'share_id': share['id'], 'share_id': share['id'],
'snapshot_id': share['snapshot_id'], 'snapshot_id': share['snapshot_id'],
'share_type': share_type, 'share_type': share_type,
'consistency_group': consistency_group,
} }
if host: if host:
@ -333,6 +392,13 @@ class API(base.Base):
msg = _("Share still has %d dependent snapshots") % len(snapshots) msg = _("Share still has %d dependent snapshots") % len(snapshots)
raise exception.InvalidShare(reason=msg) 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: try:
reservations = QUOTAS.reserve(context, reservations = QUOTAS.reserve(context,
project_id=project_id, project_id=project_id,

View File

@ -44,7 +44,16 @@ def stub_share(id, **kwargs):
'snapshot_support': True, 'snapshot_support': True,
} }
share.update(kwargs) 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): 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, def stub_snapshot_get_all_by_project(self, context, search_opts=None,
sort_key=None, sort_dir=None): sort_key=None, sort_dir=None):
return [stub_snapshot_get(self, context, 2)] 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 six
import webob import webob
from manila.api.openstack import wsgi
from manila.common import constants from manila.common import constants
from manila import context from manila import context
from manila import db from manila import db
@ -95,6 +96,27 @@ class AdminActionsTest(test.TestCase):
'/v1/fake/share_instances/%s/action' % instance['id']) '/v1/fake/share_instances/%s/action' % instance['id'])
return instance, req 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, def _reset_status(self, ctxt, model, req, db_access_method,
valid_code, valid_status=None, body=None): valid_code, valid_status=None, body=None):
if body is None: if body is None:
@ -149,6 +171,26 @@ class AdminActionsTest(test.TestCase):
self._reset_status(ctxt, instance, req, db.share_instance_get, self._reset_status(ctxt, instance, req, db.share_instance_get,
valid_code, valid_status) 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) @ddt.data(*fixture_invalid_reset_status_body)
def test_share_invalid_reset_status_body(self, body): def test_share_invalid_reset_status_body(self, body):
share, req = self._setup_share_data() share, req = self._setup_share_data()
@ -244,3 +286,23 @@ class AdminActionsTest(test.TestCase):
ctxt = self._get_context('admin') ctxt = self._get_context('admin')
self._force_delete(ctxt, instance, req, db.share_instance_get, 404) 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' kwargs['base_url'] = 'http://localhost/v1'
use_admin_context = kwargs.pop('use_admin_context', False) use_admin_context = kwargs.pop('use_admin_context', False)
version = kwargs.pop('version', api_version.DEFAULT_API_VERSION) version = kwargs.pop('version', api_version.DEFAULT_API_VERSION)
experimental = kwargs.pop('experimental', False)
out = os_wsgi.Request.blank(*args, **kwargs) out = os_wsgi.Request.blank(*args, **kwargs)
out.environ['manila.context'] = FakeRequestContext( out.environ['manila.context'] = FakeRequestContext(
'fake_user', 'fake_user',
'fake', 'fake',
is_admin=use_admin_context) 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 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.assertTrue(share_networks.QUOTAS.reserve.called)
self.assertFalse(share_networks.QUOTAS.commit.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() share_nw = fake_share_network.copy()
self.mock_object(db_api, 'share_network_get', self.mock_object(db_api, 'share_network_get',
mock.Mock(return_value=share_nw)) mock.Mock(return_value=share_nw))
@ -283,6 +283,21 @@ class ShareNetworkAPITest(test.TestCase):
assert_called_once_with(self.req.environ['manila.context'], assert_called_once_with(self.req.environ['manila.context'],
share_nw['id']) 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): def test_show_nominal(self):
share_nw = 'fake network id' share_nw = 'fake network id'
with mock.patch.object(db_api, 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) expected = self._get_expected_share_detailed_response(self.share)
self.assertEqual(expected, res_dict) 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): def test_share_create_with_valid_default_share_type(self):
self.mock_object(share_types, 'get_share_type_by_name', self.mock_object(share_types, 'get_share_type_by_name',
mock.Mock(return_value=self.vt)) mock.Mock(return_value=self.vt))
@ -337,6 +350,14 @@ class ShareApiTest(test.TestCase):
expected = self._get_expected_share_detailed_response() expected = self._get_expected_share_detailed_response()
self.assertEqual(expected, res_dict) 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): def test_share_show_admin(self):
req = fakes.HTTPRequest.blank('/shares/1', use_admin_context=True) req = fakes.HTTPRequest.blank('/shares/1', use_admin_context=True)
res_dict = self.controller.show(req, '1') res_dict = self.controller.show(req, '1')
@ -356,6 +377,35 @@ class ShareApiTest(test.TestCase):
resp = self.controller.delete(req, 1) resp = self.controller.delete(req, 1)
self.assertEqual(resp.status_int, 202) 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): def test_share_update(self):
shr = self.share shr = self.share
body = {"share": shr} body = {"share": shr}
@ -368,6 +418,15 @@ class ShareApiTest(test.TestCase):
self.assertEqual(shr['is_public'], self.assertEqual(shr['is_public'],
res_dict['share']['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): def test_share_not_updates_size(self):
req = fakes.HTTPRequest.blank('/share/1') req = fakes.HTTPRequest.blank('/share/1')
res_dict = self.controller.update(req, 1, {"share": self.share}) 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'], self.assertEqual(res_dict['shares'][0]['volume_type'],
res_dict['shares'][0]['share_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): def test_remove_invalid_options(self):
ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False) ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False)
search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'} 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): def _verify_share_server_handling(self, driver_handles_share_servers):
return super(FakeShareDriver, self)._verify_share_server_handling( return super(FakeShareDriver, self)._verify_share_server_handling(
driver_handles_share_servers) 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: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:force_delete": "rule:admin_api",
"share_extension:share_instance_admin_actions:reset_status": "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_manage": "",
"share_extension:types_extra_specs": "", "share_extension:types_extra_specs": "",
"share_extension:share_type_access": "", "share_extension:share_type_access": "",
@ -58,5 +62,16 @@
"limits_extension:used_limits": "", "limits_extension:used_limits": "",
"scheduler_stats:pools:index": "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

@ -40,6 +40,74 @@ from manila import utils
CONF = cfg.CONF 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 = [ _FAKE_LIST_OF_ALL_SHARES = [
{ {
'name': 'foo', 'name': 'foo',
@ -883,7 +951,8 @@ class ShareAPITestCase(test.TestCase):
self.api.create_instance.assert_called_once_with( self.api.create_instance.assert_called_once_with(
self.context, share, share_network_id=share['share_network_id'], self.context, share, share_network_id=share['share_network_id'],
host=valid_host, 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( share_api.policy.check_policy.assert_called_once_with(
self.context, 'share', 'create') self.context, 'share', 'create')
quota.QUOTAS.reserve.assert_called_once_with( quota.QUOTAS.reserve.assert_called_once_with(
@ -957,7 +1026,18 @@ class ShareAPITestCase(test.TestCase):
utils.IsAMatcher(context.RequestContext), share['id']) utils.IsAMatcher(context.RequestContext), share['id'])
def test_delete_wrong_status(self): 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.assertRaises(exception.InvalidShare, self.api.delete,
self.context, share) self.context, share)

View File

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