From 680fd50d3e6637d0f584983be44f291af6f49efe Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 12 Aug 2015 13:00:58 -0400 Subject: [PATCH] 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 --- etc/manila/policy.json | 17 +- manila/api/common.py | 40 + manila/api/contrib/admin_actions.py | 74 +- manila/api/openstack/api_version_request.py | 7 +- .../openstack/rest_api_version_history.rst | 6 + manila/api/openstack/versioned_method.py | 2 +- manila/api/v1/cgsnapshots.py | 198 +++ manila/api/v1/consistency_groups.py | 211 +++ manila/api/v1/router.py | 19 + manila/api/v1/share_networks.py | 10 + manila/api/v1/shares.py | 30 +- manila/api/views/cgsnapshots.py | 100 ++ manila/api/views/consistency_groups.py | 82 ++ manila/api/views/shares.py | 11 + manila/consistency_group/__init__.py | 0 manila/consistency_group/api.py | 343 +++++ manila/exception.py | 5 + manila/share/api.py | 72 +- manila/tests/api/contrib/stubs.py | 31 +- .../tests/api/contrib/test_admin_actions.py | 62 + manila/tests/api/fakes.py | 4 +- manila/tests/api/v1/test_cgsnapshots.py | 427 ++++++ .../tests/api/v1/test_consistency_groups.py | 505 +++++++ manila/tests/api/v1/test_share_networks.py | 17 +- manila/tests/api/v1/test_shares.py | 106 ++ manila/tests/consistency_group/__init__.py | 0 manila/tests/consistency_group/test_api.py | 1293 +++++++++++++++++ manila/tests/fake_driver.py | 6 + manila/tests/policy.json | 17 +- manila/tests/share/test_api.py | 84 +- manila_tempest_tests/config.py | 2 +- 31 files changed, 3757 insertions(+), 24 deletions(-) create mode 100644 manila/api/v1/cgsnapshots.py create mode 100644 manila/api/v1/consistency_groups.py create mode 100644 manila/api/views/cgsnapshots.py create mode 100644 manila/api/views/consistency_groups.py create mode 100644 manila/consistency_group/__init__.py create mode 100644 manila/consistency_group/api.py create mode 100644 manila/tests/api/v1/test_cgsnapshots.py create mode 100644 manila/tests/api/v1/test_consistency_groups.py create mode 100644 manila/tests/consistency_group/__init__.py create mode 100644 manila/tests/consistency_group/test_api.py diff --git a/etc/manila/policy.json b/etc/manila/policy.json index 3370be87..b7afba1a 100644 --- a/etc/manila/policy.json +++ b/etc/manila/policy.json @@ -40,6 +40,10 @@ "share_extension:snapshot_admin_actions:reset_status": "rule:admin_api", "share_extension:share_instance_admin_actions:force_delete": "rule:admin_api", "share_extension:share_instance_admin_actions:reset_status": "rule:admin_api", + "share_extension:consistency_group_admin_actions:force_delete": "rule:admin_api", + "share_extension:consistency_group_admin_actions:reset_status": "rule:admin_api", + "share_extension:cgsnapshot_admin_actions:force_delete": "rule:admin_api", + "share_extension:cgsnapshot_admin_actions:reset_status": "rule:admin_api", "share_extension:services": "rule:admin_api", "share_extension:availability_zones": "", @@ -78,5 +82,16 @@ "share_network:get_all_share_networks": "rule:admin_api", "scheduler_stats:pools:index": "rule:admin_api", - "scheduler_stats:pools:detail": "rule:admin_api" + "scheduler_stats:pools:detail": "rule:admin_api", + + "consistency_group:create" : "rule:default", + "consistency_group:delete": "rule:default", + "consistency_group:update": "rule:default", + "consistency_group:get": "rule:default", + "consistency_group:get_all": "rule:default", + + "consistency_group:create_cgsnapshot" : "rule:default", + "consistency_group:delete_cgsnapshot": "rule:default", + "consistency_group:get_cgsnapshot": "rule:default", + "consistency_group:get_all_cgsnapshots": "rule:default" } diff --git a/manila/api/common.py b/manila/api/common.py index bff25448..456b03bd 100644 --- a/manila/api/common.py +++ b/manila/api/common.py @@ -22,6 +22,8 @@ import six from six.moves.urllib import parse import webob +from manila.api.openstack import api_version_request as api_version +from manila.api.openstack import versioned_method from manila.i18n import _ api_common_opts = [ @@ -201,6 +203,7 @@ class ViewBuilder(object): """Model API responses as dictionaries.""" _collection_name = None + _detail_version_modifiers = [] def _get_links(self, request, identifier): return [{"rel": "self", @@ -262,6 +265,43 @@ class ViewBuilder(object): url_parts[0:2] = prefix_parts[0:2] return parse.urlunsplit(url_parts) + def update_versioned_resource_dict(self, request, resource_dict, resource): + """Updates teh given resource dict for the given request version. + + This method calls every method, that is applicable to the request + version, in _detail_version_modifiers. + """ + for method_name in self._detail_version_modifiers: + method = getattr(self, method_name) + if request.api_version_request.matches_versioned_method(method): + method.func(self, resource_dict, resource) + + @classmethod + def versioned_method(cls, min_ver, max_ver=None, experimental=False): + """Decorator for versioning API methods. + + :param min_ver: string representing minimum version + :param max_ver: optional string representing maximum version + :param experimental: flag indicating an API is experimental and is + subject to change or removal at any time + """ + + def decorator(f): + obj_min_ver = api_version.APIVersionRequest(min_ver) + if max_ver: + obj_max_ver = api_version.APIVersionRequest(max_ver) + else: + obj_max_ver = api_version.APIVersionRequest() + + # Add to list of versioned methods registered + func_name = f.__name__ + new_func = versioned_method.VersionedMethod( + func_name, obj_min_ver, obj_max_ver, experimental, f) + + return new_func + + return decorator + def remove_invalid_options(context, search_options, allowed_search_options): """Remove search options that are not valid for non-admin API/context.""" diff --git a/manila/api/contrib/admin_actions.py b/manila/api/contrib/admin_actions.py index a61a7a0f..60c96c6a 100644 --- a/manila/api/contrib/admin_actions.py +++ b/manila/api/contrib/admin_actions.py @@ -20,6 +20,7 @@ from webob import exc from manila.api import extensions from manila.api.openstack import wsgi from manila.common import constants +import manila.consistency_group.api as cg_api from manila import db from manila import exception from manila import share @@ -42,8 +43,9 @@ class AdminController(wsgi.Controller): def __init__(self, *args, **kwargs): super(AdminController, self).__init__(*args, **kwargs) - self.resource_name = self.collection.rstrip('s') + self.resource_name = self.collection.rstrip('s').replace('-', '_') self.share_api = share.API() + self.cg_api = cg_api.API() def _update(self, *args, **kwargs): raise NotImplementedError() @@ -143,18 +145,80 @@ class SnapshotAdminController(AdminController): return self.share_api.delete_snapshot(*args, **kwargs) +class CGAdminController(AdminController): + """AdminController for Consistency Groups.""" + + collection = 'consistency-groups' + + def __init__(self, *args, **kwargs): + super(CGAdminController, self).__init__(*args, **kwargs) + self.cg_api = cg_api.API() + + def _update(self, *args, **kwargs): + db.consistency_group_update(*args, **kwargs) + + def _get(self, *args, **kwargs): + return self.cg_api.get(*args, **kwargs) + + def _delete(self, context, resource, force=True): + db.consistency_group_destroy(context.elevated(), resource['id']) + + @wsgi.action('os-reset_status') + @wsgi.response(202) + @wsgi.Controller.api_version('1.5', experimental=True) + def cg_reset_status(self, req, id, body): + super(CGAdminController, self)._reset_status(req, id, body) + + @wsgi.action('os-force_delete') + @wsgi.response(202) + @wsgi.Controller.api_version('1.5', experimental=True) + def cg_force_delete(self, req, id, body): + super(CGAdminController, self)._force_delete(req, id, body) + + +class CGSnapshotAdminController(AdminController): + """AdminController for CGSnapshots.""" + + collection = 'cgsnapshots' + + def __init__(self, *args, **kwargs): + super(CGSnapshotAdminController, self).__init__(*args, **kwargs) + self.cg_api = cg_api.API() + + def _update(self, *args, **kwargs): + db.cgsnapshot_update(*args, **kwargs) + + def _get(self, *args, **kwargs): + return self.cg_api.get_cgsnapshot(*args, **kwargs) + + def _delete(self, context, resource, force=True): + db.cgsnapshot_destroy(context.elevated(), resource['id']) + + @wsgi.action('os-reset_status') + @wsgi.response(202) + @wsgi.Controller.api_version('1.5', experimental=True) + def cgsnapshot_reset_status(self, req, id, body): + super(CGSnapshotAdminController, self)._reset_status(req, id, body) + + @wsgi.action('os-force_delete') + @wsgi.response(202) + @wsgi.Controller.api_version('1.5', experimental=True) + def cgsnapshot_force_delete(self, req, id, body): + super(CGSnapshotAdminController, self)._force_delete(req, id, body) + + class Admin_actions(extensions.ExtensionDescriptor): """Enable admin actions.""" name = "AdminActions" alias = "os-admin-actions" - updated = "2015-08-03T00:00:00+00:00" + updated = "2015-09-01T00:00:00+00:00" def get_controller_extensions(self): exts = [] - controllers = (ShareAdminController, SnapshotAdminController, - ShareInstancesAdminController) - for class_ in controllers: + for class_ in (ShareAdminController, SnapshotAdminController, + ShareInstancesAdminController, + CGAdminController, CGSnapshotAdminController): controller = class_() extension = extensions.ControllerExtension( self, class_.collection, controller) diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index a98c4b4e..ddcfd3f8 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -49,6 +49,7 @@ REST_API_VERSION_HISTORY = """ * 1.2 - Share create() doesn't ignore availability_zone field of share. * 1.3 - Snapshots become optional feature. * 1.4 - Share instances admin API + * 1.5 - Consistency Group support """ @@ -56,7 +57,7 @@ REST_API_VERSION_HISTORY = """ # The default api version request is defined to be the # the minimum version of the API supported. _MIN_API_VERSION = "1.0" -_MAX_API_VERSION = "1.4" +_MAX_API_VERSION = "1.5" DEFAULT_API_VERSION = _MIN_API_VERSION @@ -79,11 +80,11 @@ class APIVersionRequest(utils.ComparableMixin): API microversions. """ - def __init__(self, version_string=None): + def __init__(self, version_string=None, experimental=False): """Create an API version request object.""" self._ver_major = None self._ver_minor = None - self._experimental = False + self._experimental = experimental if version_string is not None: match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$", diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index 1ae85523..49b7e061 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -44,3 +44,9 @@ user documentation. 1.4 --- Share instances admin API and update of Admin Actions extension. + +1.5 +--- + Consistency groups support. /consistency-groups and /cgsnapshots are + implemented. AdminActions 'os-force_delete and' 'os-reset_status' have been + updated for both new resources. diff --git a/manila/api/openstack/versioned_method.py b/manila/api/openstack/versioned_method.py index ed096bbb..3c6da780 100644 --- a/manila/api/openstack/versioned_method.py +++ b/manila/api/openstack/versioned_method.py @@ -46,4 +46,4 @@ class VersionedMethod(utils.ComparableMixin): def _cmpkey(self): """Return the value used by ComparableMixin for rich comparisons.""" - return self.start_version \ No newline at end of file + return self.start_version diff --git a/manila/api/v1/cgsnapshots.py b/manila/api/v1/cgsnapshots.py new file mode 100644 index 00000000..ff4f0910 --- /dev/null +++ b/manila/api/v1/cgsnapshots.py @@ -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()) diff --git a/manila/api/v1/consistency_groups.py b/manila/api/v1/consistency_groups.py new file mode 100644 index 00000000..13d20c38 --- /dev/null +++ b/manila/api/v1/consistency_groups.py @@ -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()) diff --git a/manila/api/v1/router.py b/manila/api/v1/router.py index 7d1bef0f..437ea140 100644 --- a/manila/api/v1/router.py +++ b/manila/api/v1/router.py @@ -23,6 +23,8 @@ from oslo_log import log from manila.api import extensions import manila.api.openstack +from manila.api.v1 import cgsnapshots +from manila.api.v1 import consistency_groups from manila.api.v1 import limits from manila.api.v1 import scheduler_stats from manila.api.v1 import security_service @@ -134,3 +136,20 @@ class APIRouter(manila.api.openstack.APIRouter): controller=self.resources['scheduler_stats'], action='pools_detail', conditions={'method': ['GET']}) + + self.resources['consistency-groups'] = ( + consistency_groups.create_resource()) + mapper.resource('consistency-group', 'consistency-groups', + controller=self.resources['consistency-groups'], + collection={'detail': 'GET'}) + mapper.connect('consistency-groups', + '/{project_id}/consistency-groups/{id}/action', + controller=self.resources['consistency-groups'], + action='action', + conditions={"action": ['POST']}) + + self.resources['cgsnapshots'] = cgsnapshots.create_resource() + mapper.resource('cgsnapshot', 'cgsnapshots', + controller=self.resources['cgsnapshots'], + collection={'detail': 'GET'}, + member={'members': 'GET', 'action': 'POST'}) diff --git a/manila/api/v1/share_networks.py b/manila/api/v1/share_networks.py index 55ac05aa..eaaf5dad 100644 --- a/manila/api/v1/share_networks.py +++ b/manila/api/v1/share_networks.py @@ -80,6 +80,16 @@ class ShareNetworkController(wsgi.Controller): 'len': len(share_instances)} LOG.error(msg) raise exc.HTTPConflict(explanation=msg) + + # NOTE(ameade): Do not allow deletion of share network used by CG + cg_count = db_api.count_consistency_groups_in_share_network(context, + id) + if cg_count: + msg = _("Can not delete share network %(id)s, it has %(len)s " + "consistency group(s).") % {'id': id, 'len': cg_count} + LOG.error(msg) + raise exc.HTTPConflict(explanation=msg) + for share_server in share_network['share_servers']: self.share_rpcapi.delete_share_server(context, share_server) db_api.share_network_delete(context, id) diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index 6bd95b76..b20ad761 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -64,6 +64,22 @@ class ShareController(wsgi.Controller): try: share = self.share_api.get(context, id) + + # NOTE(ameade): If the share is in a consistency group, we require + # it's id be specified as a param. + if share.get('consistency_group_id'): + consistency_group_id = req.params.get('consistency_group_id') + if (share.get('consistency_group_id') and + not consistency_group_id): + msg = _("Must provide 'consistency_group_id' as a request " + "parameter when deleting a share in a consistency " + "group.") + raise exc.HTTPBadRequest(explanation=msg) + elif consistency_group_id != share.get('consistency_group_id'): + msg = _("The specified 'consistency_group_id' does not " + "match the consistency group id of the share.") + raise exc.HTTPBadRequest(explanation=msg) + self.share_api.delete(context, share) except exception.NotFound: raise exc.HTTPNotFound() @@ -133,6 +149,7 @@ class ShareController(wsgi.Controller): 'display_name', 'status', 'share_server_id', 'volume_type_id', 'share_type_id', 'snapshot_id', 'host', 'share_network_id', 'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir', + 'consistency_group_id', 'cgsnapshot_id' ) def update(self, req, id, body): @@ -162,14 +179,18 @@ class ShareController(wsgi.Controller): share.update(update_dict) return self._view_builder.detail(req, share) - @wsgi.Controller.api_version("1.3") + @wsgi.Controller.api_version("1.5") def create(self, req, body): return self._create(req, body) - @wsgi.Controller.api_version("1.0", "1.2") # noqa - def create(self, req, body): + @wsgi.Controller.api_version("1.0", "1.4") # noqa + def create(self, req, body): # pylint: disable=E0102 + # Remove consistency group attributes + share = body.get('share', {}) + if 'consistency_group_id' in share: + del body['share']['consistency_group_id'] + share = self._create(req, body) - share.pop('snapshot_support', None) return share def _create(self, req, body): @@ -211,6 +232,7 @@ class ShareController(wsgi.Controller): 'availability_zone': availability_zone, 'metadata': share.get('metadata'), 'is_public': share.get('is_public', False), + 'consistency_group_id': share.get('consistency_group_id') } snapshot_id = share.get('snapshot_id') diff --git a/manila/api/views/cgsnapshots.py b/manila/api/views/cgsnapshots.py new file mode 100644 index 00000000..a76029ef --- /dev/null +++ b/manila/api/views/cgsnapshots.py @@ -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 diff --git a/manila/api/views/consistency_groups.py b/manila/api/views/consistency_groups.py new file mode 100644 index 00000000..81386ab1 --- /dev/null +++ b/manila/api/views/consistency_groups.py @@ -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 diff --git a/manila/api/views/shares.py b/manila/api/views/shares.py index 913f21d8..0eb00507 100644 --- a/manila/api/views/shares.py +++ b/manila/api/views/shares.py @@ -20,6 +20,7 @@ class ViewBuilder(common.ViewBuilder): """Model a server API response as a python dictionary.""" _collection_name = 'shares' + _detail_version_modifiers = ["add_consistency_group_fields"] def summary_list(self, request, shares): """Show a list of shares without many details.""" @@ -76,10 +77,20 @@ class ViewBuilder(common.ViewBuilder): 'is_public': share.get('is_public'), 'export_locations': export_locations, } + + self.update_versioned_resource_dict(request, share_dict, share) + if context.is_admin: share_dict['share_server_id'] = share.get('share_server_id') return {'share': share_dict} + @common.ViewBuilder.versioned_method("1.5") + def add_consistency_group_fields(self, share_dict, share): + share_dict['consistency_group_id'] = share.get( + 'consistency_group_id') + share_dict['source_cgsnapshot_member_id'] = share.get( + 'source_cgsnapshot_member_id') + def _list_view(self, func, request, shares): """Provide a view for a list of shares.""" shares_list = [func(request, share)['share'] for share in shares] diff --git a/manila/consistency_group/__init__.py b/manila/consistency_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila/consistency_group/api.py b/manila/consistency_group/api.py new file mode 100644 index 00000000..de923f07 --- /dev/null +++ b/manila/consistency_group/api.py @@ -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 diff --git a/manila/exception.py b/manila/exception.py index 585426d4..218bc2a8 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -131,6 +131,11 @@ class PolicyNotAuthorized(NotAuthorized): message = _("Policy doesn't allow %(action)s to be performed.") +class Conflict(ManilaException): + message = _("%(err)s") + code = 409 + + class Invalid(ManilaException): message = _("Unacceptable parameters.") code = 400 diff --git a/manila/share/api.py b/manila/share/api.py index f46ec501..32c37f8a 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -66,7 +66,8 @@ class API(base.Base): def create(self, context, share_proto, size, name, description, snapshot=None, availability_zone=None, metadata=None, - share_network_id=None, share_type=None, is_public=False): + share_network_id=None, share_type=None, is_public=False, + consistency_group_id=None, cgsnapshot_member=None): """Create new share.""" policy.check_policy(context, 'share', 'create') @@ -167,6 +168,49 @@ class API(base.Base): except ValueError as e: raise exception.InvalidParameterValue(six.text_type(e)) + consistency_group = None + if consistency_group_id: + try: + consistency_group = self.db.consistency_group_get( + context, consistency_group_id) + except exception.NotFound as e: + raise exception.InvalidParameterValue(six.text_type(e)) + + if (not cgsnapshot_member and + not (consistency_group['status'] == + constants.STATUS_AVAILABLE)): + params = { + 'avail': constants.STATUS_AVAILABLE, + 'cg_status': consistency_group['status'], + } + msg = _("Consistency group status must be %(avail)s, got" + "%(cg_status)s.") % params + raise exception.InvalidConsistencyGroup(message=msg) + + if share_type_id: + cg_st_ids = [st['share_type_id'] for st in + consistency_group.get('share_types', [])] + if share_type_id not in cg_st_ids: + params = { + 'type': share_type_id, + 'cg': consistency_group_id + } + msg = _("The specified share type (%(type)s) is not " + "supported by the specified consistency group " + "(%(cg)s).") % params + raise exception.InvalidParameterValue(msg) + + if (not consistency_group.get('share_network_id') + == share_network_id): + params = { + 'net': share_network_id, + 'cg': consistency_group_id + } + msg = _("The specified share network (%(net)s) is not " + "supported by the specified consistency group " + "(%(cg)s).") % params + raise exception.InvalidParameterValue(msg) + options = {'size': size, 'user_id': context.user_id, 'project_id': context.project_id, @@ -178,7 +222,10 @@ class API(base.Base): 'share_proto': share_proto, 'share_type_id': share_type_id, 'is_public': is_public, + 'consistency_group_id': consistency_group_id, } + if cgsnapshot_member: + options['source_cgsnapshot_member_id'] = cgsnapshot_member['id'] try: share = self.db.share_create(context, options, @@ -198,12 +245,15 @@ class API(base.Base): host = snapshot['share']['host'] self.create_instance(context, share, share_network_id=share_network_id, - host=host, availability_zone=availability_zone) + host=host, availability_zone=availability_zone, + consistency_group=consistency_group, + cgsnapshot_member=cgsnapshot_member) return share def create_instance(self, context, share, share_network_id=None, - host=None, availability_zone=None): + host=None, availability_zone=None, + consistency_group=None, cgsnapshot_member=None): policy.check_policy(context, 'share', 'create') availability_zone_id = None @@ -226,6 +276,14 @@ class API(base.Base): } ) + if cgsnapshot_member: + host = cgsnapshot_member['share']['host'] + share = self.db.share_instance_update(context, + share_instance['id'], + {'host': host}) + # NOTE(ameade): Do not cast to driver if creating from cgsnapshot + return + share_dict = share.to_dict() share_dict.update( {'metadata': self.db.share_metadata_get(context, share['id'])} @@ -243,6 +301,7 @@ class API(base.Base): 'share_id': share['id'], 'snapshot_id': share['snapshot_id'], 'share_type': share_type, + 'consistency_group': consistency_group, } if host: @@ -333,6 +392,13 @@ class API(base.Base): msg = _("Share still has %d dependent snapshots") % len(snapshots) raise exception.InvalidShare(reason=msg) + cgsnapshot_members_count = self.db.count_cgsnapshot_members_in_share( + context, share_id) + if cgsnapshot_members_count: + msg = (_("Share still has %d dependent cgsnapshot members") % + cgsnapshot_members_count) + raise exception.InvalidShare(reason=msg) + try: reservations = QUOTAS.reserve(context, project_id=project_id, diff --git a/manila/tests/api/contrib/stubs.py b/manila/tests/api/contrib/stubs.py index ea505a4a..47b15b6a 100644 --- a/manila/tests/api/contrib/stubs.py +++ b/manila/tests/api/contrib/stubs.py @@ -44,7 +44,16 @@ def stub_share(id, **kwargs): 'snapshot_support': True, } share.update(kwargs) - return share + + # NOTE(ameade): We must wrap the dictionary in an class in order to stub + # object attributes. + class wrapper(dict): + pass + fake_share = wrapper() + fake_share.instance = {'id': "fake_instance_id"} + fake_share.update(share) + + return fake_share def stub_snapshot(id, **kwargs): @@ -124,3 +133,23 @@ def stub_snapshot_delete(self, context, *args, **param): def stub_snapshot_get_all_by_project(self, context, search_opts=None, sort_key=None, sort_dir=None): return [stub_snapshot_get(self, context, 2)] + + +def stub_cgsnapshot_member(id, **kwargs): + member = { + 'id': id, + 'share_id': 'fakeshareid', + 'share_instance_id': 'fakeshareinstanceid', + 'share_proto': 'fakesnapproto', + 'share_type_id': 'fake_share_type_id', + 'export_location': 'fakesnaplocation', + 'user_id': 'fakesnapuser', + 'project_id': 'fakesnapproject', + 'host': 'fakesnaphost', + 'share_size': 1, + 'size': 1, + 'status': 'fakesnapstatus', + 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), + } + member.update(kwargs) + return member diff --git a/manila/tests/api/contrib/test_admin_actions.py b/manila/tests/api/contrib/test_admin_actions.py index 2ce1988b..aeaa470c 100644 --- a/manila/tests/api/contrib/test_admin_actions.py +++ b/manila/tests/api/contrib/test_admin_actions.py @@ -19,6 +19,7 @@ from oslo_serialization import jsonutils import six import webob +from manila.api.openstack import wsgi from manila.common import constants from manila import context from manila import db @@ -95,6 +96,27 @@ class AdminActionsTest(test.TestCase): '/v1/fake/share_instances/%s/action' % instance['id']) return instance, req + def _setup_cg_data(self, cg=None): + if cg is None: + cg = db_utils.create_consistency_group( + status=constants.STATUS_AVAILABLE) + req = webob.Request.blank('/v1/fake/consistency-groups/%s/action' % + cg['id']) + req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '1.5' + req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True' + + return cg, req + + def _setup_cgsnapshot_data(self, cgsnapshot=None): + if cgsnapshot is None: + cgsnapshot = db_utils.create_cgsnapshot( + 'fake_id', status=constants.STATUS_AVAILABLE) + req = webob.Request.blank('/v1/fake/cgsnapshots/%s/action' % + cgsnapshot['id']) + req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '1.5' + req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True' + return cgsnapshot, req + def _reset_status(self, ctxt, model, req, db_access_method, valid_code, valid_status=None, body=None): if body is None: @@ -149,6 +171,26 @@ class AdminActionsTest(test.TestCase): self._reset_status(ctxt, instance, req, db.share_instance_get, valid_code, valid_status) + @ddt.data(*fixture_reset_status_with_different_roles) + @ddt.unpack + def test_consistency_groups_reset_status_with_different_roles( + self, role, valid_code, valid_status): + ctxt = self._get_context(role) + cg, req = self._setup_cg_data() + + self._reset_status(ctxt, cg, req, db.consistency_group_get, + valid_code, valid_status) + + @ddt.data(*fixture_reset_status_with_different_roles) + @ddt.unpack + def test_cgsnapshot_reset_status_with_different_roles( + self, role, valid_code, valid_status): + ctxt = self._get_context(role) + cgsnap, req = self._setup_cgsnapshot_data() + + self._reset_status(ctxt, cgsnap, req, db.cgsnapshot_get, + valid_code, valid_status) + @ddt.data(*fixture_invalid_reset_status_body) def test_share_invalid_reset_status_body(self, body): share, req = self._setup_share_data() @@ -244,3 +286,23 @@ class AdminActionsTest(test.TestCase): ctxt = self._get_context('admin') self._force_delete(ctxt, instance, req, db.share_instance_get, 404) + + @ddt.data(*fixture_force_delete_with_different_roles) + @ddt.unpack + def test_consistency_group_force_delete_with_different_roles(self, role, + resp_code): + cg, req = self._setup_cg_data() + ctxt = self._get_context(role) + + self._force_delete(ctxt, cg, req, db.consistency_group_get, + resp_code) + + @ddt.data(*fixture_force_delete_with_different_roles) + @ddt.unpack + def test_cgsnapshot_force_delete_with_different_roles(self, role, + resp_code): + cgsnap, req = self._setup_cgsnapshot_data() + ctxt = self._get_context(role) + + self._force_delete(ctxt, cgsnap, req, db.cgsnapshot_get, + resp_code) diff --git a/manila/tests/api/fakes.py b/manila/tests/api/fakes.py index eed31593..1184bf0b 100644 --- a/manila/tests/api/fakes.py +++ b/manila/tests/api/fakes.py @@ -111,12 +111,14 @@ class HTTPRequest(os_wsgi.Request): kwargs['base_url'] = 'http://localhost/v1' use_admin_context = kwargs.pop('use_admin_context', False) version = kwargs.pop('version', api_version.DEFAULT_API_VERSION) + experimental = kwargs.pop('experimental', False) out = os_wsgi.Request.blank(*args, **kwargs) out.environ['manila.context'] = FakeRequestContext( 'fake_user', 'fake', is_admin=use_admin_context) - out.api_version_request = api_version.APIVersionRequest(version) + out.api_version_request = api_version.APIVersionRequest( + version, experimental=experimental) return out diff --git a/manila/tests/api/v1/test_cgsnapshots.py b/manila/tests/api/v1/test_cgsnapshots.py new file mode 100644 index 00000000..574c2a2d --- /dev/null +++ b/manila/tests/api/v1/test_cgsnapshots.py @@ -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']) diff --git a/manila/tests/api/v1/test_consistency_groups.py b/manila/tests/api/v1/test_consistency_groups.py new file mode 100644 index 00000000..a74b8c43 --- /dev/null +++ b/manila/tests/api/v1/test_consistency_groups.py @@ -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']) diff --git a/manila/tests/api/v1/test_share_networks.py b/manila/tests/api/v1/test_share_networks.py index a0dd8245..87fd6517 100644 --- a/manila/tests/api/v1/test_share_networks.py +++ b/manila/tests/api/v1/test_share_networks.py @@ -265,7 +265,7 @@ class ShareNetworkAPITest(test.TestCase): self.assertTrue(share_networks.QUOTAS.reserve.called) self.assertFalse(share_networks.QUOTAS.commit.called) - def test_delete_in_use(self): + def test_delete_in_use_by_share(self): share_nw = fake_share_network.copy() self.mock_object(db_api, 'share_network_get', mock.Mock(return_value=share_nw)) @@ -283,6 +283,21 @@ class ShareNetworkAPITest(test.TestCase): assert_called_once_with(self.req.environ['manila.context'], share_nw['id']) + def test_delete_in_use_by_consistency_group(self): + share_nw = fake_share_network.copy() + self.mock_object(db_api, 'share_network_get', + mock.Mock(return_value=share_nw)) + self.mock_object(db_api, 'count_consistency_groups_in_share_network', + mock.Mock(return_value=2)) + + self.assertRaises(webob_exc.HTTPConflict, + self.controller.delete, + self.req, + share_nw['id']) + + db_api.share_network_get.assert_called_once_with( + self.req.environ['manila.context'], share_nw['id']) + def test_show_nominal(self): share_nw = 'fake network id' with mock.patch.object(db_api, diff --git a/manila/tests/api/v1/test_shares.py b/manila/tests/api/v1/test_shares.py index 2a4069af..8f7fd892 100644 --- a/manila/tests/api/v1/test_shares.py +++ b/manila/tests/api/v1/test_shares.py @@ -127,6 +127,19 @@ class ShareApiTest(test.TestCase): expected = self._get_expected_share_detailed_response(self.share) self.assertEqual(expected, res_dict) + def test_share_create_with_consistency_group(self): + self.mock_object(share_api.API, 'create', self.create_mock) + + body = {"share": copy.deepcopy(self.share)} + req = fakes.HTTPRequest.blank('/shares', version="1.5") + res_dict = self.controller.create(req, body) + + expected = self._get_expected_share_detailed_response(self.share) + + expected['share']['consistency_group_id'] = None + expected['share']['source_cgsnapshot_member_id'] = None + self.assertEqual(expected, res_dict) + def test_share_create_with_valid_default_share_type(self): self.mock_object(share_types, 'get_share_type_by_name', mock.Mock(return_value=self.vt)) @@ -337,6 +350,14 @@ class ShareApiTest(test.TestCase): expected = self._get_expected_share_detailed_response() self.assertEqual(expected, res_dict) + def test_share_show_with_consistency_group(self): + req = fakes.HTTPRequest.blank('/shares/1', version='1.5') + res_dict = self.controller.show(req, '1') + expected = self._get_expected_share_detailed_response() + expected['share']['consistency_group_id'] = None + expected['share']['source_cgsnapshot_member_id'] = None + self.assertEqual(expected, res_dict) + def test_share_show_admin(self): req = fakes.HTTPRequest.blank('/shares/1', use_admin_context=True) res_dict = self.controller.show(req, '1') @@ -356,6 +377,35 @@ class ShareApiTest(test.TestCase): resp = self.controller.delete(req, 1) self.assertEqual(resp.status_int, 202) + def test_share_delete_in_consistency_group_param_not_provided(self): + fake_share = stubs.stub_share('fake_share', + consistency_group_id='fake_cg_id') + self.mock_object(share_api.API, 'get', + mock.Mock(return_value=fake_share)) + req = fakes.HTTPRequest.blank('/shares/1') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.delete, req, 1) + + def test_share_delete_in_consistency_group(self): + fake_share = stubs.stub_share('fake_share', + consistency_group_id='fake_cg_id') + self.mock_object(share_api.API, 'get', + mock.Mock(return_value=fake_share)) + req = fakes.HTTPRequest.blank( + '/shares/1?consistency_group_id=fake_cg_id') + resp = self.controller.delete(req, 1) + self.assertEqual(resp.status_int, 202) + + def test_share_delete_in_consistency_group_wrong_id(self): + fake_share = stubs.stub_share('fake_share', + consistency_group_id='fake_cg_id') + self.mock_object(share_api.API, 'get', + mock.Mock(return_value=fake_share)) + req = fakes.HTTPRequest.blank( + '/shares/1?consistency_group_id=not_fake_cg_id') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.delete, req, 1) + def test_share_update(self): shr = self.share body = {"share": shr} @@ -368,6 +418,15 @@ class ShareApiTest(test.TestCase): self.assertEqual(shr['is_public'], res_dict['share']['is_public']) + def test_share_update_with_consistency_group(self): + shr = self.share + body = {"share": shr} + + req = fakes.HTTPRequest.blank('/share/1', version="1.5") + res_dict = self.controller.update(req, 1, body) + self.assertIsNone(res_dict['share']["consistency_group_id"]) + self.assertIsNone(res_dict['share']["source_cgsnapshot_member_id"]) + def test_share_not_updates_size(self): req = fakes.HTTPRequest.blank('/share/1') res_dict = self.controller.update(req, 1, {"share": self.share}) @@ -600,6 +659,53 @@ class ShareApiTest(test.TestCase): self.assertEqual(res_dict['shares'][0]['volume_type'], res_dict['shares'][0]['share_type']) + def test_share_list_detail_with_consistency_group(self): + self.mock_object(share_api.API, 'get_all', + stubs.stub_share_get_all_by_project) + env = {'QUERY_STRING': 'name=Share+Test+Name'} + req = fakes.HTTPRequest.blank('/shares/detail', environ=env, + version="1.5") + res_dict = self.controller.detail(req) + expected = { + 'shares': [ + { + 'status': 'fakestatus', + 'description': 'displaydesc', + 'export_location': 'fake_location', + 'export_locations': ['fake_location', 'fake_location2'], + 'availability_zone': 'fakeaz', + 'name': 'displayname', + 'share_proto': 'FAKEPROTO', + 'metadata': {}, + 'project_id': 'fakeproject', + 'host': 'fakehost', + 'id': '1', + 'snapshot_id': '2', + 'share_network_id': None, + 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'size': 1, + 'share_type': '1', + 'volume_type': '1', + 'is_public': False, + 'consistency_group_id': None, + 'source_cgsnapshot_member_id': None, + 'links': [ + { + 'href': 'http://localhost/v1/fake/shares/1', + 'rel': 'self' + }, + { + 'href': 'http://localhost/fake/shares/1', + 'rel': 'bookmark' + } + ], + } + ] + } + self.assertEqual(expected, res_dict) + self.assertEqual(res_dict['shares'][0]['volume_type'], + res_dict['shares'][0]['share_type']) + def test_remove_invalid_options(self): ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False) search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'} diff --git a/manila/tests/consistency_group/__init__.py b/manila/tests/consistency_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila/tests/consistency_group/test_api.py b/manila/tests/consistency_group/test_api.py new file mode 100644 index 00000000..32dc2fe4 --- /dev/null +++ b/manila/tests/consistency_group/test_api.py @@ -0,0 +1,1293 @@ +# 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. +"""Unit tests for the Share API module.""" + +import copy +import datetime + +import ddt +import mock +from oslo_config import cfg +from oslo_utils import timeutils + +from manila.common import constants +import manila.consistency_group.api as cg_api +from manila import context +from manila import db as db_driver +from manila import exception +from manila.share import share_types +from manila import test +from manila.tests.api.contrib import stubs + +CONF = cfg.CONF + + +def fake_cg(id, **kwargs): + cg = { + 'id': 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_types': None, + 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), + } + cg.update(kwargs) + return cg + + +def fake_cgsnapshot(id, **kwargs): + snap = { + 'id': 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(kwargs) + return snap + + +@ddt.ddt +class CGAPITestCase(test.TestCase): + + def setUp(self): + super(CGAPITestCase, self).setUp() + self.context = context.get_admin_context() + self.scheduler_rpcapi = mock.Mock() + self.share_rpcapi = mock.Mock() + self.share_api = mock.Mock() + self.api = cg_api.API() + self.mock_object(self.api, 'share_rpcapi', self.share_rpcapi) + self.mock_object(self.api, 'share_api', self.share_api) + self.mock_object(self.api, 'scheduler_rpcapi', self.scheduler_rpcapi) + + dt_utc = datetime.datetime.utcnow() + self.mock_object(timeutils, 'utcnow', mock.Mock(return_value=dt_utc)) + self.mock_object(cg_api.policy, 'check_policy') + + def test_create_empty_request(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + self.api.create(self.context) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + + def test_create_policy_check(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + self.api.create(self.context) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'create') + + def test_create_request_spec(self): + """Ensure the correct values are sent to the scheduler.""" + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + expected_request_spec = { + 'consistency_group_id': cg['id'], + } + expected_request_spec.update(cg) + del expected_request_spec['id'] + del expected_request_spec['created_at'] + del expected_request_spec['host'] + expected_request_spec['share_types'] = [] + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + + self.api.create(self.context) + + self.scheduler_rpcapi.create_consistency_group.assert_called_once_with( + self.context, cg_id=cg['id'], request_spec=expected_request_spec, + filter_properties={} + ) + + def test_create_with_name(self): + fake_name = 'fake_name' + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + expected_values['name'] = fake_name + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'share_network_get') + + self.api.create(self.context, name=fake_name) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + self.scheduler_rpcapi.create_consistency_group.assert_called_once_with( + self.context, cg_id=cg['id'], request_spec=mock.ANY, + filter_properties={} + ) + + def test_create_with_description(self): + fake_desc = 'fake_desc' + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + expected_values['description'] = fake_desc + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + + self.api.create(self.context, description=fake_desc) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + + def test_create_with_multiple_share_types(self): + fake_share_type = {'name': 'default', + 'extra_specs': { + 'driver_handles_share_servers': 'False'}, + 'is_public': True, + 'id': 'c01990c1-448f-435a-9de6-c7c894bb6df9'} + fake_share_type_2 = {'name': 'default2', + 'extra_specs': { + 'driver_handles_share_servers': 'False'}, + 'is_public': True, + 'id': 'c01990c1-448f-435a-9de6-c7c894bb7df9'} + fake_share_types = [fake_share_type, fake_share_type_2] + self.mock_object(share_types, 'get_share_type') + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + expected_values['share_types'] = fake_share_types + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'share_network_get') + + self.api.create(self.context, share_type_ids=fake_share_types) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + + def test_create_with_share_type_not_found(self): + fake_share_type = {'name': 'default', + 'extra_specs': { + 'driver_handles_share_servers': 'False'}, + 'is_public': True, + 'id': 'c01990c1-448f-435a-9de6-c7c894bb6df9'} + fake_share_types = [fake_share_type] + self.mock_object(share_types, 'get_share_type', + mock.Mock(side_effect=exception.ShareTypeNotFound( + share_type_id=fake_share_type['id']))) + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + expected_values['share_types'] = fake_share_types + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + + self.assertRaises(exception.InvalidInput, self.api.create, + self.context, share_type_ids=[fake_share_type['id']]) + + def test_create_driver_handles_share_servers_is_false_with_net_id(self): + fake_share_type = {'name': 'default', + 'extra_specs': { + 'driver_handles_share_servers': 'False'}, + 'is_public': False, + 'id': 'c01990c1-448f-435a-9de6-c7c894bb6df9'} + + fake_share_types = [fake_share_type] + self.mock_object(share_types, 'get_share_type') + + self.assertRaises(exception.InvalidInput, self.api.create, + self.context, share_type_ids=fake_share_types, + share_network_id="fake_share_network") + + def test_create_with_conflicting_share_types(self): + fake_share_type = {'name': 'default', + 'extra_specs': { + 'driver_handles_share_servers': 'True'}, + 'is_public': True, + 'id': 'c01990c1-448f-435a-9de6-c7c894bb6df9'} + fake_share_type_2 = {'name': 'default2', + 'extra_specs': { + 'driver_handles_share_servers': 'False'}, + 'is_public': True, + 'id': 'c01990c1-448f-435a-9de6-c7c894bb7df9'} + fake_share_types = [fake_share_type, fake_share_type_2] + self.mock_object(share_types, 'get_share_type', + mock.Mock(side_effect=[fake_share_type, + fake_share_type_2])) + + self.assertRaises(exception.InvalidInput, self.api.create, + self.context, share_type_ids=fake_share_types) + + def test_create_with_conflicting_share_type_and_share_network(self): + fake_share_type = {'name': 'default', + 'extra_specs': { + 'driver_handles_share_servers': 'False'}, + 'is_public': True, + 'id': 'c01990c1-448f-435a-9de6-c7c894bb6df9'} + fake_share_types = [fake_share_type] + self.mock_object(share_types, 'get_share_type', + mock.Mock(return_value=fake_share_type)) + + self.assertRaises(exception.InvalidInput, self.api.create, + self.context, share_type_ids=fake_share_types, + share_network_id="fake_sn") + + def test_create_with_source_cgsnapshot_id(self): + snap = fake_cgsnapshot("fake_source_cgsnapshot_id", + status=constants.STATUS_AVAILABLE) + fake_share_type_mapping = {'share_type_id': "fake_share_type_id"} + orig_cg = fake_cg('fakeorigid', + user_id=self.context.user_id, + project_id=self.context.project_id, + share_types=[fake_share_type_mapping], + status=constants.STATUS_AVAILABLE, + host='fake_original_host') + + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + share_types=[fake_share_type_mapping], + status=constants.STATUS_CREATING, + host='fake_original_host') + expected_values = cg.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + expected_values['source_cgsnapshot_id'] = snap['id'] + expected_values['share_types'] = ["fake_share_type_id"] + + self.mock_object(db_driver, 'cgsnapshot_get', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=orig_cg)) + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + self.mock_object(share_types, 'get_share_type', + mock.Mock(return_value={"id": "fake_share_type_id"})) + self.mock_object(db_driver, 'share_network_get') + self.mock_object(db_driver, 'cgsnapshot_members_get_all', + mock.Mock(return_value=[])) + + self.api.create(self.context, + source_cgsnapshot_id=snap['id']) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + self.share_rpcapi.create_consistency_group.\ + assert_called_once_with(self.context, cg, orig_cg['host']) + + def test_create_with_source_cgsnapshot_id_with_member(self): + snap = fake_cgsnapshot("fake_source_cgsnapshot_id", + status=constants.STATUS_AVAILABLE) + share = stubs.stub_share('fakeshareid') + member = stubs.stub_cgsnapshot_member('fake_member_id') + fake_share_type_mapping = {'share_type_id': "fake_share_type_id"} + orig_cg = fake_cg('fakeorigid', + user_id=self.context.user_id, + project_id=self.context.project_id, + share_types=[fake_share_type_mapping], + status=constants.STATUS_AVAILABLE) + + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + share_types=[fake_share_type_mapping], + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + expected_values['source_cgsnapshot_id'] = snap['id'] + expected_values['share_types'] = ["fake_share_type_id"] + + self.mock_object(db_driver, 'cgsnapshot_get', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=orig_cg)) + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + self.mock_object(share_types, 'get_share_type', + mock.Mock(return_value={"id": "fake_share_type_id"})) + self.mock_object(db_driver, 'share_network_get') + self.mock_object(db_driver, 'share_instance_get', + mock.Mock(return_value=share)) + self.mock_object(db_driver, 'cgsnapshot_members_get_all', + mock.Mock(return_value=[member])) + self.mock_object(self.share_api, 'create') + + self.api.create(self.context, + source_cgsnapshot_id=snap['id']) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + self.assertTrue(self.share_api.create.called) + self.share_rpcapi.create_consistency_group.\ + assert_called_once_with(self.context, cg, orig_cg['host']) + + def test_create_with_source_cgsnapshot_id_with_members_error(self): + snap = fake_cgsnapshot("fake_source_cgsnapshot_id", + status=constants.STATUS_AVAILABLE) + member = stubs.stub_cgsnapshot_member('fake_member_id') + member_2 = stubs.stub_cgsnapshot_member('fake_member2_id') + share = stubs.stub_share('fakeshareid') + fake_share_type_mapping = {'share_type_id': "fake_share_type_id"} + orig_cg = fake_cg('fakeorigid', + user_id=self.context.user_id, + project_id=self.context.project_id, + share_types=[fake_share_type_mapping], + status=constants.STATUS_AVAILABLE) + + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + share_types=[fake_share_type_mapping], + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + expected_values['source_cgsnapshot_id'] = snap['id'] + expected_values['share_types'] = ["fake_share_type_id"] + + self.mock_object(db_driver, 'cgsnapshot_get', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=orig_cg)) + self.mock_object(db_driver, 'share_network_get') + self.mock_object(db_driver, 'share_instance_get', + mock.Mock(return_value=share)) + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + self.mock_object(share_types, 'get_share_type', + mock.Mock(return_value={"id": "fake_share_type_id"})) + self.mock_object(db_driver, 'cgsnapshot_members_get_all', + mock.Mock(return_value=[member, member_2])) + self.mock_object(self.share_api, 'create', + mock.Mock(side_effect=[None, exception.Error])) + self.mock_object(db_driver, 'consistency_group_destroy') + + self.assertRaises(exception.Error, self.api.create, self.context, + source_cgsnapshot_id=snap['id']) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + self.assertEqual(2, self.share_api.create.call_count) + self.assertEqual(1, db_driver.consistency_group_destroy.call_count) + + def test_create_with_source_cgsnapshot_id_error_snapshot_status(self): + snap = fake_cgsnapshot("fake_source_cgsnapshot_id", + status=constants.STATUS_ERROR) + self.mock_object(db_driver, 'cgsnapshot_get', + mock.Mock(return_value=snap)) + + self.assertRaises(exception.InvalidCGSnapshot, self.api.create, + self.context, source_cgsnapshot_id=snap['id']) + + def test_create_with_source_cgsnapshot_id_snap_not_found(self): + snap = fake_cgsnapshot("fake_source_cgsnapshot_id", + status=constants.STATUS_ERROR) + self.mock_object(db_driver, 'cgsnapshot_get', + mock.Mock(side_effect=exception.CGSnapshotNotFound( + cgsnapshot_id='fake_source_cgsnapshot_id' + ))) + + self.assertRaises(exception.CGSnapshotNotFound, self.api.create, + self.context, source_cgsnapshot_id=snap['id']) + + def test_create_with_multiple_fields(self): + fake_desc = 'fake_desc' + fake_name = 'fake_name' + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + expected_values['name'] = fake_name + expected_values['description'] = fake_desc + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(return_value=cg)) + + self.api.create(self.context, name=fake_name, + description=fake_desc) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + + def test_create_with_error_on_creation(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = cg.copy() + for name in ('id', 'host', 'created_at'): + expected_values.pop(name, None) + + self.mock_object(db_driver, 'consistency_group_create', + mock.Mock(side_effect=exception.Error)) + + self.assertRaises(exception.Error, self.api.create, self.context) + + db_driver.consistency_group_create.assert_called_once_with( + self.context, expected_values) + + def test_delete_creating_no_host(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'consistency_group_destroy') + + self.api.delete(self.context, cg) + + db_driver.consistency_group_destroy.assert_called_once_with( + mock.ANY, cg['id']) + + def test_delete_policy_check(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'consistency_group_destroy') + + self.api.delete(self.context, cg) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'delete', cg) + + def test_delete_creating_with_host(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING, + host="fake_host") + + self.assertRaises(exception.InvalidConsistencyGroup, self.api.delete, + self.context, cg) + + def test_delete_available(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE, + host="fake_host") + deleted_cg = copy.deepcopy(cg) + deleted_cg['status'] = constants.STATUS_DELETING + self.mock_object(db_driver, 'consistency_group_update', + mock.Mock(return_value=deleted_cg)) + self.mock_object(db_driver, 'count_shares_in_consistency_group', + mock.Mock(return_value=0)) + + self.api.delete(self.context, cg) + + db_driver.consistency_group_update.assert_called_once_with( + self.context, cg['id'], {'status': constants.STATUS_DELETING}) + self.share_rpcapi.delete_consistency_group.assert_called_once_with( + self.context, deleted_cg + ) + + def test_delete_error_with_host(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_ERROR, + host="fake_host") + deleted_cg = copy.deepcopy(cg) + deleted_cg['status'] = constants.STATUS_DELETING + self.mock_object(self.api, 'share_rpcapi') + self.mock_object(db_driver, 'consistency_group_update', + mock.Mock(return_value=deleted_cg)) + self.mock_object(db_driver, 'count_shares_in_consistency_group', + mock.Mock(return_value=0)) + + self.api.delete(self.context, cg) + + db_driver.consistency_group_update.assert_called_once_with( + self.context, cg['id'], {'status': constants.STATUS_DELETING}) + self.api.share_rpcapi.delete_consistency_group.assert_called_once_with( + self.context, deleted_cg + ) + + def test_delete_error_without_host(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_ERROR) + self.mock_object(db_driver, 'consistency_group_destroy') + + self.api.delete(self.context, cg) + + db_driver.consistency_group_destroy.assert_called_once_with( + mock.ANY, cg['id']) + + def test_delete_with_shares(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE, + host="fake_host") + self.mock_object(db_driver, 'count_shares_in_consistency_group', + mock.Mock(return_value=1)) + + self.assertRaises(exception.InvalidConsistencyGroup, self.api.delete, + self.context, cg) + + def test_delete_with_cgsnapshots(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE, + host="fake_host") + self.mock_object(db_driver, 'count_cgsnapshots_in_consistency_group', + mock.Mock(return_value=1)) + + self.assertRaises(exception.InvalidConsistencyGroup, self.api.delete, + self.context, cg) + + def test_update_policy_check(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = {} + + self.mock_object(db_driver, 'consistency_group_update', + mock.Mock(return_value=cg)) + + self.api.update(self.context, cg, expected_values) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'update', cg) + + def test_update_no_values(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = {} + self.mock_object(db_driver, 'consistency_group_update', + mock.Mock(return_value=cg)) + + self.api.update(self.context, cg, expected_values) + + db_driver.consistency_group_update.assert_called_once_with( + self.context, cg['id'], expected_values) + + def test_update_with_name(self): + fake_name = 'fake_name' + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = {'description': fake_name} + + self.mock_object(db_driver, 'consistency_group_update', + mock.Mock(return_value=cg)) + + self.api.update(self.context, cg, expected_values) + + db_driver.consistency_group_update.assert_called_once_with( + self.context, cg['id'], expected_values) + + def test_update_with_description(self): + fake_desc = 'fake_desc' + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = {'description': fake_desc} + + self.mock_object(db_driver, 'consistency_group_update', + mock.Mock(return_value=cg)) + + self.api.update(self.context, cg, expected_values) + + db_driver.consistency_group_update.assert_called_once_with( + self.context, cg['id'], expected_values) + + def test_get_policy_check(self): + cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + + self.api.get(self.context, cg['id']) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'get') + + def test_get(self): + expected_cg = fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=expected_cg)) + + actual_cg = self.api.get(self.context, expected_cg['id']) + self.assertEqual(expected_cg, actual_cg) + + def test_get_all_policy_check(self): + self.mock_object(db_driver, 'consistency_group_get_all', + mock.Mock(return_value=[])) + + self.api.get_all(self.context) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'get_all') + + def test_get_all_no_cgs(self): + + self.mock_object(db_driver, 'consistency_group_get_all', + mock.Mock(return_value=[])) + + actual_cg = self.api.get_all(self.context) + self.assertEqual([], actual_cg) + + def test_get_all(self): + expected_cgs = [fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING)] + self.mock_object(db_driver, 'consistency_group_get_all_by_project', + mock.Mock(return_value=expected_cgs)) + + actual_cg = self.api.get_all(self.context, detailed=True) + self.assertEqual(expected_cgs, actual_cg) + + def test_get_all_all_tenants_not_admin(self): + cxt = context.RequestContext(user_id=None, + project_id=None, + is_admin=False) + expected_cgs = [fake_cg('fakeid', + user_id=cxt.user_id, + project_id=cxt.project_id, + status=constants.STATUS_CREATING)] + self.mock_object(db_driver, 'consistency_group_get_all_by_project', + mock.Mock(return_value=expected_cgs)) + + actual_cgs = self.api.get_all(cxt, + search_opts={'all_tenants': True}) + self.assertEqual(expected_cgs, actual_cgs) + + def test_get_all_all_tenants_as_admin(self): + expected_cgs = [fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING)] + self.mock_object(db_driver, 'consistency_group_get_all', + mock.Mock(return_value=expected_cgs)) + + actual_cgs = self.api.get_all(self.context, + search_opts={'all_tenants': True}) + self.assertEqual(expected_cgs, actual_cgs) + db_driver.consistency_group_get_all.assert_called_once_with( + self.context, detailed=True) + + def test_create_cgsnapshot_policy_check(self): + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + + self.api.create_cgsnapshot(self.context) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'create_cgsnapshot') + + def test_create_cgsnapshot_minimal_request_no_members(self): + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + consistency_group_id=cg['id'], + status=constants.STATUS_CREATING) + expected_values = snap.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[])) + + self.api.create_cgsnapshot(self.context, consistency_group_id=cg['id']) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + db_driver.cgsnapshot_create.assert_called_once_with( + self.context, expected_values) + self.share_rpcapi.create_cgsnapshot.assert_called_once_with( + self.context, snap, cg['host'] + ) + + def test_create_cgsnapshot_minimal_request_no_members_with_name(self): + fake_name = 'fake_name' + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + consistency_group_id=cg['id'], + name=fake_name, + status=constants.STATUS_CREATING) + expected_values = snap.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[])) + + self.api.create_cgsnapshot(self.context, consistency_group_id=cg['id'], + name=fake_name) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + db_driver.cgsnapshot_create.assert_called_once_with( + self.context, expected_values) + self.share_rpcapi.create_cgsnapshot.assert_called_once_with( + self.context, snap, cg['host'] + ) + + def test_create_cgsnapshot_minimal_request_no_members_with_desc(self): + fake_description = 'fake_description' + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + consistency_group_id=cg['id'], + description=fake_description, + status=constants.STATUS_CREATING) + expected_values = snap.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[])) + + self.api.create_cgsnapshot(self.context, consistency_group_id=cg['id'], + description=fake_description) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + db_driver.cgsnapshot_create.assert_called_once_with( + self.context, expected_values) + self.share_rpcapi.create_cgsnapshot.assert_called_once_with( + self.context, snap, cg['host'] + ) + + def test_create_cgsnapshot_cg_does_not_exist(self): + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + consistency_group_id=cg['id'], + status=constants.STATUS_CREATING) + expected_values = snap.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[])) + + self.assertRaises(exception.InvalidConsistencyGroup, + self.api.create_cgsnapshot, + self.context, + consistency_group_id=cg['id']) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + + def test_create_cgsnapshot_cg_in_creating(self): + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock( + side_effect=exception.ConsistencyGroupNotFound( + consistency_group_id='fake_id' + ))) + + self.assertRaises(exception.ConsistencyGroupNotFound, + self.api.create_cgsnapshot, + self.context, + consistency_group_id="fake_id") + + db_driver.consistency_group_get.assert_called_once_with( + self.context, "fake_id" + ) + + def test_create_cgsnapshot_with_member(self): + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + consistency_group_id=cg['id'], + status=constants.STATUS_CREATING) + share = stubs.stub_share('fake_share_id', + status=constants.STATUS_AVAILABLE) + expected_values = snap.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + expected_member_values = { + 'cgsnapshot_id': snap['id'], + 'user_id': self.context.user_id, + 'project_id': self.context.project_id, + 'status': constants.STATUS_CREATING, + 'size': share['size'], + 'share_proto': share['share_proto'], + 'share_type_id': share['share_type_id'], + 'share_id': share['id'], + 'share_instance_id': mock.ANY, + } + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'cgsnapshot_member_create', + mock.Mock()) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[share])) + + self.api.create_cgsnapshot(self.context, consistency_group_id=cg['id']) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + db_driver.cgsnapshot_create.assert_called_once_with( + self.context, expected_values) + db_driver.cgsnapshot_member_create.assert_called_once_with( + self.context, expected_member_values + ) + self.share_rpcapi.create_cgsnapshot.assert_called_once_with( + self.context, snap, cg['host'] + ) + + def test_create_cgsnapshot_with_member_share_in_creating(self): + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + share = stubs.stub_share('fake_share_id', + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[share])) + + self.assertRaises(exception.InvalidConsistencyGroup, + self.api.create_cgsnapshot, + self.context, + consistency_group_id=cg['id']) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + + def test_create_cgsnapshot_with_two_members(self): + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + consistency_group_id=cg['id'], + status=constants.STATUS_CREATING) + share = stubs.stub_share('fake_share_id', + status=constants.STATUS_AVAILABLE) + share_2 = stubs.stub_share('fake_share2_id', + status=constants.STATUS_AVAILABLE) + expected_values = snap.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + expected_member_1_values = { + 'cgsnapshot_id': snap['id'], + 'user_id': self.context.user_id, + 'project_id': self.context.project_id, + 'status': constants.STATUS_CREATING, + 'size': share['size'], + 'share_proto': share['share_proto'], + 'share_type_id': share['share_type_id'], + 'share_id': share['id'], + 'share_instance_id': mock.ANY, + } + expected_member_2_values = { + 'cgsnapshot_id': snap['id'], + 'user_id': self.context.user_id, + 'project_id': self.context.project_id, + 'status': constants.STATUS_CREATING, + 'size': share_2['size'], + 'share_proto': share_2['share_proto'], + 'share_type_id': share_2['share_type_id'], + 'share_id': share_2['id'], + 'share_instance_id': mock.ANY, + } + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[share, share_2])) + self.mock_object(db_driver, 'cgsnapshot_member_create', + mock.Mock()) + + self.api.create_cgsnapshot(self.context, consistency_group_id=cg['id']) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + db_driver.cgsnapshot_create.assert_called_once_with( + self.context, expected_values) + + db_driver.cgsnapshot_member_create.assert_any_call( + self.context, expected_member_1_values + ) + db_driver.cgsnapshot_member_create.assert_any_call( + self.context, expected_member_2_values + ) + self.share_rpcapi.create_cgsnapshot.assert_called_once_with( + self.context, snap, cg['host'] + ) + + def test_create_cgsnapshot_error_creating_member(self): + cg = fake_cg('fake_cg_id', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_AVAILABLE) + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + consistency_group_id=cg['id'], + status=constants.STATUS_CREATING) + share = stubs.stub_share('fake_share_id', + status=constants.STATUS_AVAILABLE) + expected_values = snap.copy() + for name in ('id', 'created_at'): + expected_values.pop(name, None) + expected_member_values = { + 'cgsnapshot_id': snap['id'], + 'user_id': self.context.user_id, + 'project_id': self.context.project_id, + 'status': constants.STATUS_CREATING, + 'size': share['size'], + 'share_proto': share['share_proto'], + 'share_type_id': share['share_type_id'], + 'share_id': share['id'], + 'share_instance_id': mock.ANY, + } + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_create', + mock.Mock(return_value=snap)) + self.mock_object(db_driver, 'cgsnapshot_destroy') + self.mock_object(db_driver, 'cgsnapshot_member_create', + mock.Mock(side_effect=exception.Error)) + self.mock_object(db_driver, 'share_get_all_by_consistency_group_id', + mock.Mock(return_value=[share])) + + self.assertRaises(exception.Error, self.api.create_cgsnapshot, + self.context, consistency_group_id=cg['id']) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, cg['id'] + ) + db_driver.cgsnapshot_create.assert_called_once_with( + self.context, expected_values) + db_driver.cgsnapshot_member_create.assert_called_once_with( + self.context, expected_member_values + ) + db_driver.cgsnapshot_destroy.assert_called_once_with( + self.context, snap['id'] + ) + + def test_delete_cgsnapshot(self): + cg = fake_cg('fake_id', host="fake_host") + snap = fake_cgsnapshot('fake_cgsnap_id', + consistency_group_id='fake_id', + status=constants.STATUS_AVAILABLE) + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock(return_value=cg)) + self.mock_object(db_driver, 'cgsnapshot_update') + + self.api.delete_cgsnapshot(self.context, snap) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'delete_cgsnapshot', snap) + db_driver.consistency_group_get.assert_called_once_with( + self.context, "fake_id" + ) + db_driver.cgsnapshot_update.assert_called_once_with( + self.context, snap['id'], {'status': constants.STATUS_DELETING} + ) + self.share_rpcapi.delete_cgsnapshot.assert_called_once_with( + self.context, snap, cg['host']) + + def test_delete_cgsnapshot_cg_does_not_exist(self): + snap = fake_cgsnapshot('fake_cgsnap_id', + consistency_group_id='fake_id') + self.mock_object(db_driver, 'consistency_group_get', + mock.Mock( + side_effect=exception.ConsistencyGroupNotFound( + consistency_group_id='fake_id' + ))) + + self.assertRaises(exception.ConsistencyGroupNotFound, + self.api.delete_cgsnapshot, + self.context, + snap) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, "fake_id" + ) + + def test_delete_cgsnapshot_creating_status(self): + snap = fake_cgsnapshot('fake_cgsnap_id', + consistency_group_id='fake_id', + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'consistency_group_get') + + self.assertRaises(exception.InvalidCGSnapshot, + self.api.delete_cgsnapshot, + self.context, + snap) + + db_driver.consistency_group_get.assert_called_once_with( + self.context, "fake_id" + ) + + def test_update_cgsnapshot_policy_check(self): + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = {} + + self.mock_object(db_driver, 'cgsnapshot_update', + mock.Mock(return_value=snap)) + + self.api.update_cgsnapshot(self.context, snap, expected_values) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'update_cgsnapshot', snap) + + def test_update_cgsnapshot_no_values(self): + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = {} + self.mock_object(db_driver, 'cgsnapshot_update', + mock.Mock(return_value=snap)) + + self.api.update_cgsnapshot(self.context, snap, expected_values) + + db_driver.cgsnapshot_update.assert_called_once_with( + self.context, snap['id'], expected_values) + + def test_update_cgsnapshot_with_name(self): + fake_name = 'fake_name' + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + expected_values = {'description': fake_name} + + self.mock_object(db_driver, 'cgsnapshot_update', + mock.Mock(return_value=snap)) + + self.api.update_cgsnapshot(self.context, snap, expected_values) + + db_driver.cgsnapshot_update.assert_called_once_with( + self.context, snap['id'], expected_values) + + def test_cgsnapshot_get_policy_check(self): + snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'cgsnapshot_get', + mock.Mock(return_value=snap)) + + self.api.get_cgsnapshot(self.context, snap['id']) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'get_cgsnapshot') + + def test_cgsnapshot_get(self): + expected_snap = fake_cgsnapshot('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING) + self.mock_object(db_driver, 'cgsnapshot_get', + mock.Mock(return_value=expected_snap)) + + actual_cg = self.api.get_cgsnapshot(self.context, expected_snap['id']) + self.assertEqual(expected_snap, actual_cg) + + def test_cgsnapshot_get_all_policy_check(self): + self.mock_object(db_driver, 'cgsnapshot_get_all', + mock.Mock(return_value=[])) + + self.api.get_all_cgsnapshots(self.context) + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'get_all_cgsnapshots') + + def test_cgsnapshot_get_all_no_cgs(self): + + self.mock_object(db_driver, 'cgsnapshot_get_all', + mock.Mock(return_value=[])) + + actual_cg = self.api.get_all_cgsnapshots(self.context) + self.assertEqual([], actual_cg) + + def test_cgsnapshot_get_all(self): + expected_snaps = [fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING)] + self.mock_object(db_driver, 'cgsnapshot_get_all_by_project', + mock.Mock(return_value=expected_snaps)) + + actual_cg = self.api.get_all_cgsnapshots(self.context, detailed=True) + self.assertEqual(expected_snaps, actual_cg) + + def test_cgsnapshot_get_all_all_tenants_not_admin(self): + cxt = context.RequestContext(user_id=None, + project_id=None, + is_admin=False) + expected_snaps = [fake_cg('fakeid', + user_id=cxt.user_id, + project_id=cxt.project_id, + status=constants.STATUS_CREATING)] + self.mock_object(db_driver, 'cgsnapshot_get_all_by_project', + mock.Mock(return_value=expected_snaps)) + + actual_cgs = self.api.get_all_cgsnapshots( + cxt, search_opts={'all_tenants': True}) + self.assertEqual(expected_snaps, actual_cgs) + + def test_cgsnapshot_get_all_all_tenants_as_admin(self): + expected_snaps = [fake_cg('fakeid', + user_id=self.context.user_id, + project_id=self.context.project_id, + status=constants.STATUS_CREATING)] + self.mock_object(db_driver, 'cgsnapshot_get_all', + mock.Mock(return_value=expected_snaps)) + + actual_cgs = self.api.get_all_cgsnapshots( + self.context, search_opts={'all_tenants': True}) + self.assertEqual(expected_snaps, actual_cgs) + db_driver.cgsnapshot_get_all.assert_called_once_with( + self.context, detailed=True) + + def test_get_all_cgsnapshot_members_policy_check(self): + self.mock_object(db_driver, 'cgsnapshot_members_get_all', + mock.Mock(return_value=[])) + + self.api.get_all_cgsnapshot_members(self.context, 'fake_id') + + cg_api.policy.check_policy.assert_called_once_with( + self.context, 'consistency_group', 'get_cgsnapshot') + + def test_get_all_cgsnapshot_members(self): + self.mock_object(db_driver, 'cgsnapshot_members_get_all', + mock.Mock(return_value=[])) + + self.api.get_all_cgsnapshot_members(self.context, 'fake_id') + + db_driver.cgsnapshot_members_get_all.assert_called_once_with( + self.context, 'fake_id' + ) diff --git a/manila/tests/fake_driver.py b/manila/tests/fake_driver.py index b1d4fb91..391870ac 100644 --- a/manila/tests/fake_driver.py +++ b/manila/tests/fake_driver.py @@ -89,3 +89,9 @@ class FakeShareDriver(driver.ShareDriver): def _verify_share_server_handling(self, driver_handles_share_servers): return super(FakeShareDriver, self)._verify_share_server_handling( driver_handles_share_servers) + + def create_consistency_group(self, context, cg_id): + pass + + def delete_consistency_group(self, context, cg_id): + pass diff --git a/manila/tests/policy.json b/manila/tests/policy.json index 22907024..6ebaee1a 100644 --- a/manila/tests/policy.json +++ b/manila/tests/policy.json @@ -43,6 +43,10 @@ "share_extension:snapshot_admin_actions:reset_status": "rule:admin_api", "share_extension:share_instance_admin_actions:force_delete": "rule:admin_api", "share_extension:share_instance_admin_actions:reset_status": "rule:admin_api", + "share_extension:consistency_group_admin_actions:force_delete": "rule:admin_api", + "share_extension:consistency_group_admin_actions:reset_status": "rule:admin_api", + "share_extension:cgsnapshot_admin_actions:force_delete": "rule:admin_api", + "share_extension:cgsnapshot_admin_actions:reset_status": "rule:admin_api", "share_extension:types_manage": "", "share_extension:types_extra_specs": "", "share_extension:share_type_access": "", @@ -58,5 +62,16 @@ "limits_extension:used_limits": "", "scheduler_stats:pools:index": "rule:admin_api", - "scheduler_stats:pools:detail": "rule:admin_api" + "scheduler_stats:pools:detail": "rule:admin_api", + + "consistency_group:create" : "rule:default", + "consistency_group:delete": "rule:default", + "consistency_group:update": "rule:default", + "consistency_group:get": "rule:default", + "consistency_group:get_all": "rule:default", + + "consistency_group:create_cgsnapshot" : "rule:default", + "consistency_group:delete_cgsnapshot": "rule:default", + "consistency_group:get_cgsnapshot": "rule:default", + "consistency_group:get_all_cgsnapshots": "rule:default" } diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py index 90d2d7aa..e1d8a441 100644 --- a/manila/tests/share/test_api.py +++ b/manila/tests/share/test_api.py @@ -40,6 +40,74 @@ from manila import utils CONF = cfg.CONF +def fake_share(id, **kwargs): + share = { + 'id': id, + 'size': 1, + 'user_id': 'fakeuser', + 'project_id': 'fakeproject', + 'snapshot_id': None, + 'share_network_id': None, + 'share_type_id': None, + 'availability_zone': 'fakeaz', + 'status': 'fakestatus', + 'display_name': 'fakename', + 'metadata': None, + 'display_description': 'fakedesc', + 'share_proto': 'nfs', + 'export_location': 'fake_location', + 'host': 'fakehost', + 'is_public': False, + 'consistency_group_id': None, + 'scheduled_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'launched_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'terminated_at': datetime.datetime(1, 1, 1, 1, 1, 1) + } + share.update(kwargs) + return share + + +def fake_snapshot(id, **kwargs): + snapshot = { + 'id': id, + 'share_size': 1, + 'size': 1, + 'user_id': 'fakeuser', + 'project_id': 'fakeproject', + 'share_id': None, + 'availability_zone': 'fakeaz', + 'status': 'fakestatus', + 'display_name': 'fakename', + 'display_description': 'fakedesc', + 'share_proto': 'nfs', + 'progress': 'fakeprogress99%', + 'scheduled_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'launched_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'terminated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'share': {'host': 'fake_source_host'}, + } + snapshot.update(kwargs) + return snapshot + + +def fake_access(id, **kwargs): + access = { + 'id': id, + 'share_id': 'fakeshareid', + 'access_type': 'fakeacctype', + 'access_to': 'fakeaccto', + 'access_level': 'rw', + 'state': 'fakeactive', + 'STATE_NEW': 'fakenew', + 'STATE_ACTIVE': 'fakeactive', + 'STATE_DELETING': 'fakedeleting', + 'STATE_DELETED': 'fakedeleted', + 'STATE_ERROR': 'fakeerror', + } + access.update(kwargs) + return access + + _FAKE_LIST_OF_ALL_SHARES = [ { 'name': 'foo', @@ -883,7 +951,8 @@ class ShareAPITestCase(test.TestCase): self.api.create_instance.assert_called_once_with( self.context, share, share_network_id=share['share_network_id'], host=valid_host, - availability_zone=snapshot['share']['availability_zone']) + availability_zone=snapshot['share']['availability_zone'], + consistency_group=None, cgsnapshot_member=None) share_api.policy.check_policy.assert_called_once_with( self.context, 'share', 'create') quota.QUOTAS.reserve.assert_called_once_with( @@ -957,7 +1026,18 @@ class ShareAPITestCase(test.TestCase): utils.IsAMatcher(context.RequestContext), share['id']) def test_delete_wrong_status(self): - share = db_utils.create_share() + share = fake_share('fakeid') + self.mock_object(db_api, 'share_get', mock.Mock(return_value=share)) + self.assertRaises(exception.InvalidShare, self.api.delete, + self.context, share) + + @mock.patch.object(db_api, 'count_cgsnapshot_members_in_share', + mock.Mock(return_value=2)) + def test_delete_dependent_cgsnapshot_members(self): + share_server_id = 'fake-ss-id' + share = self._setup_delete_mocks(constants.STATUS_AVAILABLE, + share_server_id) + self.assertRaises(exception.InvalidShare, self.api.delete, self.context, share) diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index f6962ea4..ea1f678c 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -36,7 +36,7 @@ ShareGroup = [ help="The minimum api microversion is configured to be the " "value of the minimum microversion supported by Manila."), cfg.StrOpt("max_api_microversion", - default="1.4", + default="1.5", help="The maximum api microversion is configured to be the " "value of the latest microversion supported by Manila."), cfg.StrOpt("region",