Add Consistency Groups API
This patch adds the /consistency-groups and /cgsnapshots endpoints as well as AdminActions for both. Partially implements bp manila-consistency-groups APIImpact Change-Id: I5fd0d3341766fdba4d92f4a43c3d1186e7a4b38e
This commit is contained in:
parent
f3a761f06b
commit
680fd50d3e
@ -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"
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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)$",
|
||||
|
@ -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.
|
||||
|
198
manila/api/v1/cgsnapshots.py
Normal file
198
manila/api/v1/cgsnapshots.py
Normal file
@ -0,0 +1,198 @@
|
||||
# Copyright 2015 Alex Meade
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""The consistency groups snapshot API."""
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from manila.api import common
|
||||
from manila.api.openstack import wsgi
|
||||
import manila.api.views.cgsnapshots as cg_views
|
||||
import manila.consistency_group.api as cg_api
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila.i18n import _LI
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class CGSnapshotController(wsgi.Controller):
|
||||
"""The Consistency Group Snapshots API controller for the OpenStack API."""
|
||||
|
||||
_view_builder_class = cg_views.CGSnapshotViewBuilder
|
||||
|
||||
def __init__(self):
|
||||
super(CGSnapshotController, self).__init__()
|
||||
self.cg_api = cg_api.API()
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def show(self, req, id):
|
||||
"""Return data about the given cgsnapshot."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
try:
|
||||
cg = self.cg_api.get_cgsnapshot(context, id)
|
||||
except exception.NotFound:
|
||||
msg = _("Consistency group snapshot %s not found.") % id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
return self._view_builder.detail(req, cg)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def delete(self, req, id):
|
||||
"""Delete a cgsnapshot."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
LOG.info(_LI("Delete consistency group snapshot with id: %s"), id,
|
||||
context=context)
|
||||
|
||||
try:
|
||||
snap = self.cg_api.get_cgsnapshot(context, id)
|
||||
except exception.NotFound:
|
||||
msg = _("Consistency group snapshot %s not found.") % id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
try:
|
||||
self.cg_api.delete_cgsnapshot(context, snap)
|
||||
except exception.InvalidCGSnapshot as e:
|
||||
raise exc.HTTPConflict(explanation=six.text_type(e))
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def index(self, req):
|
||||
"""Returns a summary list of cgsnapshots."""
|
||||
return self._get_cgs(req, is_detail=False)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def detail(self, req):
|
||||
"""Returns a detailed list of cgsnapshots."""
|
||||
return self._get_cgs(req, is_detail=True)
|
||||
|
||||
def _get_cgs(self, req, is_detail):
|
||||
"""Returns a list of cgsnapshots."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
search_opts = {}
|
||||
search_opts.update(req.GET)
|
||||
|
||||
# Remove keys that are not related to cg attrs
|
||||
search_opts.pop('limit', None)
|
||||
search_opts.pop('offset', None)
|
||||
|
||||
snaps = self.cg_api.get_all_cgsnapshots(
|
||||
context, detailed=is_detail, search_opts=search_opts)
|
||||
|
||||
limited_list = common.limited(snaps, req)
|
||||
|
||||
if is_detail:
|
||||
snaps = self._view_builder.detail_list(req, limited_list)
|
||||
else:
|
||||
snaps = self._view_builder.summary_list(req, limited_list)
|
||||
return snaps
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def update(self, req, id, body):
|
||||
"""Update a cgsnapshot."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
if not self.is_valid_body(body, 'cgsnapshot'):
|
||||
msg = _("'cgsnapshot' is missing from the request body")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
cg_data = body['cgsnapshot']
|
||||
valid_update_keys = {
|
||||
'name',
|
||||
'description',
|
||||
}
|
||||
invalid_fields = set(cg_data.keys()) - valid_update_keys
|
||||
if invalid_fields:
|
||||
msg = _("The fields %s are invalid or not allowed to be updated.")
|
||||
raise exc.HTTPBadRequest(explanation=msg % invalid_fields)
|
||||
|
||||
try:
|
||||
cg = self.cg_api.get_cgsnapshot(context, id)
|
||||
except exception.NotFound:
|
||||
msg = _("Consistency group snapshot %s not found.") % id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
cg = self.cg_api.update_cgsnapshot(context, cg, cg_data)
|
||||
return self._view_builder.detail(req, cg)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
@wsgi.response(202)
|
||||
def create(self, req, body):
|
||||
"""Creates a new cgsnapshot."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
if not self.is_valid_body(body, 'cgsnapshot'):
|
||||
msg = _("'cgsnapshot' is missing from the request body")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
cgsnapshot = body.get('cgsnapshot')
|
||||
|
||||
if not cgsnapshot.get('consistency_group_id'):
|
||||
msg = _("Must supply 'consistency_group_id' attribute.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
consistency_group_id = cgsnapshot.get('consistency_group_id')
|
||||
if (consistency_group_id and
|
||||
not uuidutils.is_uuid_like(consistency_group_id)):
|
||||
msg = _("The 'consistency_group_id' attribute must be a uuid.")
|
||||
raise exc.HTTPBadRequest(explanation=six.text_type(msg))
|
||||
|
||||
kwargs = {"consistency_group_id": consistency_group_id}
|
||||
|
||||
if 'name' in cgsnapshot:
|
||||
kwargs['name'] = cgsnapshot.get('name')
|
||||
if 'description' in cgsnapshot:
|
||||
kwargs['description'] = cgsnapshot.get('description')
|
||||
|
||||
try:
|
||||
new_snapshot = self.cg_api.create_cgsnapshot(context, **kwargs)
|
||||
except exception.ConsistencyGroupNotFound as e:
|
||||
raise exc.HTTPBadRequest(explanation=six.text_type(e))
|
||||
except exception.InvalidConsistencyGroup as e:
|
||||
raise exc.HTTPConflict(explanation=six.text_type(e))
|
||||
|
||||
return self._view_builder.detail(req, dict(six.iteritems(
|
||||
new_snapshot)))
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def members(self, req, id):
|
||||
"""Returns a list of cgsnapshot members."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
search_opts = {}
|
||||
search_opts.update(req.GET)
|
||||
|
||||
# Remove keys that are not related to cg attrs
|
||||
search_opts.pop('limit', None)
|
||||
search_opts.pop('offset', None)
|
||||
|
||||
snaps = self.cg_api.get_all_cgsnapshot_members(context, id)
|
||||
|
||||
limited_list = common.limited(snaps, req)
|
||||
|
||||
snaps = self._view_builder.member_list(req, limited_list)
|
||||
return snaps
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(CGSnapshotController())
|
211
manila/api/v1/consistency_groups.py
Normal file
211
manila/api/v1/consistency_groups.py
Normal file
@ -0,0 +1,211 @@
|
||||
# Copyright 2015 Alex Meade
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""The consistency groups API."""
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from manila.api import common
|
||||
from manila.api.openstack import wsgi
|
||||
import manila.api.views.consistency_groups as cg_views
|
||||
import manila.consistency_group.api as cg_api
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila.i18n import _LI
|
||||
from manila.share import share_types
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class CGController(wsgi.Controller):
|
||||
"""The Consistency Groups API controller for the OpenStack API."""
|
||||
|
||||
_view_builder_class = cg_views.CGViewBuilder
|
||||
|
||||
def __init__(self):
|
||||
super(CGController, self).__init__()
|
||||
self.cg_api = cg_api.API()
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def show(self, req, id):
|
||||
"""Return data about the given CG."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
try:
|
||||
cg = self.cg_api.get(context, id)
|
||||
except exception.NotFound:
|
||||
msg = _("Consistency group %s not found.") % id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
return self._view_builder.detail(req, cg)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def delete(self, req, id):
|
||||
"""Delete a CG."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
LOG.info(_LI("Delete consistency group with id: %s"), id,
|
||||
context=context)
|
||||
|
||||
try:
|
||||
cg = self.cg_api.get(context, id)
|
||||
except exception.NotFound:
|
||||
msg = _("Consistency group %s not found.") % id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
try:
|
||||
self.cg_api.delete(context, cg)
|
||||
except exception.InvalidConsistencyGroup as e:
|
||||
raise exc.HTTPConflict(explanation=six.text_type(e))
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def index(self, req):
|
||||
"""Returns a summary list of shares."""
|
||||
return self._get_cgs(req, is_detail=False)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def detail(self, req):
|
||||
"""Returns a detailed list of shares."""
|
||||
return self._get_cgs(req, is_detail=True)
|
||||
|
||||
def _get_cgs(self, req, is_detail):
|
||||
"""Returns a list of shares, transformed through view builder."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
search_opts = {}
|
||||
search_opts.update(req.GET)
|
||||
|
||||
# Remove keys that are not related to cg attrs
|
||||
search_opts.pop('limit', None)
|
||||
search_opts.pop('offset', None)
|
||||
|
||||
cgs = self.cg_api.get_all(
|
||||
context, detailed=is_detail, search_opts=search_opts)
|
||||
|
||||
limited_list = common.limited(cgs, req)
|
||||
|
||||
if is_detail:
|
||||
cgs = self._view_builder.detail_list(req, limited_list)
|
||||
else:
|
||||
cgs = self._view_builder.summary_list(req, limited_list)
|
||||
return cgs
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
def update(self, req, id, body):
|
||||
"""Update a share."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
if not self.is_valid_body(body, 'consistency_group'):
|
||||
msg = _("'consistency_group' is missing from the request body.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
cg_data = body['consistency_group']
|
||||
valid_update_keys = {
|
||||
'name',
|
||||
'description',
|
||||
}
|
||||
invalid_fields = set(cg_data.keys()) - valid_update_keys
|
||||
if invalid_fields:
|
||||
msg = _("The fields %s are invalid or not allowed to be updated.")
|
||||
raise exc.HTTPBadRequest(explanation=msg % invalid_fields)
|
||||
|
||||
try:
|
||||
cg = self.cg_api.get(context, id)
|
||||
except exception.NotFound:
|
||||
msg = _("Consistency group %s not found.") % id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
cg = self.cg_api.update(context, cg, cg_data)
|
||||
return self._view_builder.detail(req, cg)
|
||||
|
||||
@wsgi.Controller.api_version('1.5', experimental=True)
|
||||
@wsgi.response(202)
|
||||
def create(self, req, body):
|
||||
"""Creates a new share."""
|
||||
context = req.environ['manila.context']
|
||||
|
||||
if not self.is_valid_body(body, 'consistency_group'):
|
||||
msg = _("'consistency_group' is missing from the request body.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
cg = body['consistency_group']
|
||||
|
||||
valid_fields = {'name', 'description', 'share_types',
|
||||
'source_cgsnapshot_id', 'share_network_id'}
|
||||
invalid_fields = set(cg.keys()) - valid_fields
|
||||
if invalid_fields:
|
||||
msg = _("The fields %s are invalid.") % invalid_fields
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if 'share_types' in cg and 'source_cgsnapshot_id' in cg:
|
||||
msg = _("Cannot supply both 'share_types' and "
|
||||
"'source_cgsnapshot_id' attributes.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if not cg.get('share_types') and 'source_cgsnapshot_id' not in cg:
|
||||
default_share_type = share_types.get_default_share_type()
|
||||
if default_share_type:
|
||||
cg['share_types'] = [default_share_type['id']]
|
||||
else:
|
||||
msg = _("Must specify at least one share type as a default "
|
||||
"share type has not been configured.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
kwargs = {}
|
||||
|
||||
if 'name' in cg:
|
||||
kwargs['name'] = cg.get('name')
|
||||
if 'description' in cg:
|
||||
kwargs['description'] = cg.get('description')
|
||||
|
||||
_share_types = cg.get('share_types')
|
||||
if _share_types:
|
||||
if not all([uuidutils.is_uuid_like(st) for st in _share_types]):
|
||||
msg = _("The 'share_types' attribute must be a list of uuids")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
kwargs['share_type_ids'] = _share_types
|
||||
|
||||
if 'source_cgsnapshot_id' in cg:
|
||||
source_cgsnapshot_id = cg.get('source_cgsnapshot_id')
|
||||
if not uuidutils.is_uuid_like(source_cgsnapshot_id):
|
||||
msg = _("The 'source_cgsnapshot_id' attribute must be a uuid.")
|
||||
raise exc.HTTPBadRequest(explanation=six.text_type(msg))
|
||||
kwargs['source_cgsnapshot_id'] = source_cgsnapshot_id
|
||||
|
||||
if 'share_network_id' in cg:
|
||||
share_network_id = cg.get('share_network_id')
|
||||
if not uuidutils.is_uuid_like(share_network_id):
|
||||
msg = _("The 'share_network_id' attribute must be a uuid.")
|
||||
raise exc.HTTPBadRequest(explanation=six.text_type(msg))
|
||||
kwargs['share_network_id'] = share_network_id
|
||||
|
||||
try:
|
||||
new_cg = self.cg_api.create(context, **kwargs)
|
||||
except exception.InvalidCGSnapshot as e:
|
||||
raise exc.HTTPConflict(explanation=six.text_type(e))
|
||||
except (exception.CGSnapshotNotFound, exception.InvalidInput) as e:
|
||||
raise exc.HTTPBadRequest(explanation=six.text_type(e))
|
||||
|
||||
return self._view_builder.detail(req, dict(six.iteritems(new_cg)))
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(CGController())
|
@ -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'})
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
100
manila/api/views/cgsnapshots.py
Normal file
100
manila/api/views/cgsnapshots.py
Normal file
@ -0,0 +1,100 @@
|
||||
# Copyright 2015 Alex Meade
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""The consistency groups snapshot API."""
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from manila.api import common
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class CGSnapshotViewBuilder(common.ViewBuilder):
|
||||
"""Model a cgsnapshot API response as a python dictionary."""
|
||||
|
||||
_collection_name = 'cgsnapshot'
|
||||
|
||||
def summary_list(self, request, cgs):
|
||||
"""Show a list of cgsnapshots without many details."""
|
||||
return self._list_view(self.summary, request, cgs)
|
||||
|
||||
def detail_list(self, request, cgs):
|
||||
"""Detailed view of a list of cgsnapshots."""
|
||||
return self._list_view(self.detail, request, cgs)
|
||||
|
||||
def member_list(self, request, members):
|
||||
members_list = []
|
||||
for member in members:
|
||||
member_dict = {
|
||||
'id': member.get('id'),
|
||||
'created_at': member.get('created_at'),
|
||||
'size': member.get('size'),
|
||||
'share_protocol': member.get('share_proto'),
|
||||
'project_id': member.get('project_id'),
|
||||
'share_type_id': member.get('share_type_id'),
|
||||
'cgsnapshot_id': member.get('cgsnapshot_id'),
|
||||
'share_id': member.get('share_id'),
|
||||
}
|
||||
members_list.append(member_dict)
|
||||
|
||||
members_links = self._get_collection_links(request,
|
||||
members,
|
||||
'cgsnapshot_id')
|
||||
members_dict = dict(cgsnapshot_members=members_list)
|
||||
|
||||
if members_links:
|
||||
members_dict['cgsnapshot_members_links'] = members_links
|
||||
|
||||
return members_dict
|
||||
|
||||
def summary(self, request, cg):
|
||||
"""Generic, non-detailed view of a cgsnapshot."""
|
||||
return {
|
||||
'cgsnapshot': {
|
||||
'id': cg.get('id'),
|
||||
'name': cg.get('name'),
|
||||
'links': self._get_links(request, cg['id'])
|
||||
}
|
||||
}
|
||||
|
||||
def detail(self, request, cg):
|
||||
"""Detailed view of a single cgsnapshot."""
|
||||
cg_dict = {
|
||||
'id': cg.get('id'),
|
||||
'name': cg.get('name'),
|
||||
'created_at': cg.get('created_at'),
|
||||
'status': cg.get('status'),
|
||||
'description': cg.get('description'),
|
||||
'project_id': cg.get('project_id'),
|
||||
'consistency_group_id': cg.get('consistency_group_id'),
|
||||
'links': self._get_links(request, cg['id']),
|
||||
}
|
||||
return {'cgsnapshot': cg_dict}
|
||||
|
||||
def _list_view(self, func, request, snaps):
|
||||
"""Provide a view for a list of cgsnapshots."""
|
||||
snap_list = [func(request, snap)['cgsnapshot']
|
||||
for snap in snaps]
|
||||
snaps_links = self._get_collection_links(request,
|
||||
snaps,
|
||||
self._collection_name)
|
||||
snaps_dict = dict(cgsnapshots=snap_list)
|
||||
|
||||
if snaps_links:
|
||||
snaps_dict['cgsnapshot_links'] = snaps_links
|
||||
|
||||
return snaps_dict
|
82
manila/api/views/consistency_groups.py
Normal file
82
manila/api/views/consistency_groups.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Copyright 2015 Alex Meade
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""The consistency groups API."""
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from manila.api import common
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class CGViewBuilder(common.ViewBuilder):
|
||||
"""Model a consistency group API response as a python dictionary."""
|
||||
|
||||
_collection_name = 'consistency_groups'
|
||||
|
||||
def summary_list(self, request, cgs):
|
||||
"""Show a list of consistency groups without many details."""
|
||||
return self._list_view(self.summary, request, cgs)
|
||||
|
||||
def detail_list(self, request, cgs):
|
||||
"""Detailed view of a list of consistency groups."""
|
||||
return self._list_view(self.detail, request, cgs)
|
||||
|
||||
def summary(self, request, cg):
|
||||
"""Generic, non-detailed view of a consistency group."""
|
||||
return {
|
||||
'consistency_group': {
|
||||
'id': cg.get('id'),
|
||||
'name': cg.get('name'),
|
||||
'links': self._get_links(request, cg['id'])
|
||||
}
|
||||
}
|
||||
|
||||
def detail(self, request, cg):
|
||||
"""Detailed view of a single consistency group."""
|
||||
context = request.environ['manila.context']
|
||||
cg_dict = {
|
||||
'id': cg.get('id'),
|
||||
'name': cg.get('name'),
|
||||
'created_at': cg.get('created_at'),
|
||||
'status': cg.get('status'),
|
||||
'description': cg.get('description'),
|
||||
'project_id': cg.get('project_id'),
|
||||
'host': cg.get('host'),
|
||||
'source_cgsnapshot_id': cg.get('source_cgsnapshot_id'),
|
||||
'share_network_id': cg.get('share_network_id'),
|
||||
'share_types': [st['share_type_id'] for st in cg.get(
|
||||
'share_types')],
|
||||
'links': self._get_links(request, cg['id']),
|
||||
}
|
||||
if context.is_admin:
|
||||
cg_dict['share_server_id'] = cg_dict.get('share_server_id')
|
||||
return {'consistency_group': cg_dict}
|
||||
|
||||
def _list_view(self, func, request, shares):
|
||||
"""Provide a view for a list of consistency groups."""
|
||||
cg_list = [func(request, share)['consistency_group']
|
||||
for share in shares]
|
||||
cgs_links = self._get_collection_links(request,
|
||||
shares,
|
||||
self._collection_name)
|
||||
cgs_dict = dict(consistency_groups=cg_list)
|
||||
|
||||
if cgs_links:
|
||||
cgs_dict['consistency_groups_links'] = cgs_links
|
||||
|
||||
return cgs_dict
|
@ -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]
|
||||
|
0
manila/consistency_group/__init__.py
Normal file
0
manila/consistency_group/__init__.py
Normal file
343
manila/consistency_group/api.py
Normal file
343
manila/consistency_group/api.py
Normal file
@ -0,0 +1,343 @@
|
||||
# Copyright (c) 2015 Alex Meade
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Handles all requests relating to consistency groups.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
|
||||
from manila.common import constants
|
||||
from manila.db import base
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila import policy
|
||||
from manila.scheduler import rpcapi as scheduler_rpcapi
|
||||
from manila import share
|
||||
from manila.share import rpcapi as share_rpcapi
|
||||
from manila.share import share_types
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class API(base.Base):
|
||||
"""API for interacting with the share manager."""
|
||||
|
||||
def __init__(self, db_driver=None):
|
||||
self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
|
||||
self.share_rpcapi = share_rpcapi.ShareAPI()
|
||||
self.share_api = share.API()
|
||||
super(API, self).__init__(db_driver)
|
||||
|
||||
def create(self, context, name=None, description=None,
|
||||
share_type_ids=None, source_cgsnapshot_id=None,
|
||||
share_network_id=None):
|
||||
"""Create new consistency group."""
|
||||
policy.check_policy(context, 'consistency_group', 'create')
|
||||
|
||||
cgsnapshot = None
|
||||
original_cg = None
|
||||
if source_cgsnapshot_id:
|
||||
cgsnapshot = self.db.cgsnapshot_get(context, source_cgsnapshot_id)
|
||||
if cgsnapshot['status'] != constants.STATUS_AVAILABLE:
|
||||
msg = (_("Consistency group snapshot status must be %s")
|
||||
% constants.STATUS_AVAILABLE)
|
||||
raise exception.InvalidCGSnapshot(reason=msg)
|
||||
|
||||
original_cg = self.db.consistency_group_get(context, cgsnapshot[
|
||||
'consistency_group_id'])
|
||||
share_type_ids = [s['share_type_id'] for s in original_cg[
|
||||
'share_types']]
|
||||
|
||||
# Get share_type_objects
|
||||
share_type_objects = []
|
||||
driver_handles_share_servers = None
|
||||
for share_type_id in (share_type_ids or []):
|
||||
try:
|
||||
share_type_object = share_types.get_share_type(
|
||||
context, share_type_id)
|
||||
except exception.ShareTypeNotFound:
|
||||
msg = _("Share type with id %s could not be found")
|
||||
raise exception.InvalidInput(msg % share_type_id)
|
||||
share_type_objects.append(share_type_object)
|
||||
|
||||
extra_specs = share_type_object.get('extra_specs')
|
||||
if extra_specs:
|
||||
share_type_handle_ss = strutils.bool_from_string(
|
||||
extra_specs.get(
|
||||
constants.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS))
|
||||
if driver_handles_share_servers is None:
|
||||
driver_handles_share_servers = share_type_handle_ss
|
||||
elif not driver_handles_share_servers == share_type_handle_ss:
|
||||
# NOTE(ameade): if the share types have conflicting values
|
||||
# for driver_handles_share_servers then raise bad request
|
||||
msg = _("The specified share_types cannot have "
|
||||
"conflicting values for the "
|
||||
"driver_handles_share_servers extra spec.")
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
if (not share_type_handle_ss) and share_network_id:
|
||||
msg = _("When using a share types with the "
|
||||
"driver_handles_share_servers extra spec as "
|
||||
"False, a share_network_id must not be provided.")
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
try:
|
||||
if share_network_id:
|
||||
self.db.share_network_get(context, share_network_id)
|
||||
except exception.ShareNetworkNotFound:
|
||||
msg = _("The specified share network does not exist.")
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
if (driver_handles_share_servers and
|
||||
not (source_cgsnapshot_id or share_network_id)):
|
||||
msg = _("When using a share type with the "
|
||||
"driver_handles_share_servers extra spec as"
|
||||
"True, a share_network_id must be provided.")
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
options = {
|
||||
'source_cgsnapshot_id': source_cgsnapshot_id,
|
||||
'share_network_id': share_network_id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'user_id': context.user_id,
|
||||
'project_id': context.project_id,
|
||||
'status': constants.STATUS_CREATING,
|
||||
'share_types': share_type_ids
|
||||
}
|
||||
if original_cg:
|
||||
options['host'] = original_cg['host']
|
||||
|
||||
cg = self.db.consistency_group_create(context, options)
|
||||
|
||||
try:
|
||||
if cgsnapshot:
|
||||
members = self.db.cgsnapshot_members_get_all(
|
||||
context, source_cgsnapshot_id)
|
||||
for member in members:
|
||||
share_type = share_types.get_share_type(
|
||||
context, member['share_type_id'])
|
||||
member['share'] = self.db.share_instance_get(
|
||||
context, member['share_instance_id'],
|
||||
with_share_data=True)
|
||||
self.share_api.create(context, member['share_proto'],
|
||||
member['size'], None, None,
|
||||
consistency_group_id=cg['id'],
|
||||
cgsnapshot_member=member,
|
||||
share_type=share_type,
|
||||
share_network_id=share_network_id)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
self.db.consistency_group_destroy(context.elevated(), cg['id'])
|
||||
|
||||
request_spec = {'consistency_group_id': cg['id']}
|
||||
request_spec.update(options)
|
||||
request_spec['share_types'] = share_type_objects
|
||||
|
||||
if cgsnapshot and original_cg:
|
||||
self.share_rpcapi.create_consistency_group(
|
||||
context, cg, original_cg['host'])
|
||||
else:
|
||||
self.scheduler_rpcapi.create_consistency_group(
|
||||
context, cg_id=cg['id'], request_spec=request_spec,
|
||||
filter_properties={})
|
||||
|
||||
return cg
|
||||
|
||||
@policy.wrap_check_policy('consistency_group')
|
||||
def delete(self, context, cg):
|
||||
"""Delete consistency group."""
|
||||
|
||||
cg_id = cg['id']
|
||||
if not cg['host']:
|
||||
self.db.consistency_group_destroy(context.elevated(), cg_id)
|
||||
return
|
||||
|
||||
statuses = (constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
|
||||
if not cg['status'] in statuses:
|
||||
msg = (_("Consistency group status must be one of %(statuses)s")
|
||||
% {"statuses": statuses})
|
||||
raise exception.InvalidConsistencyGroup(reason=msg)
|
||||
|
||||
# NOTE(ameade): check for cgsnapshots in the CG
|
||||
if self.db.count_cgsnapshots_in_consistency_group(context, cg_id):
|
||||
msg = (_("Cannot delete a consistency group with cgsnapshots"))
|
||||
raise exception.InvalidConsistencyGroup(reason=msg)
|
||||
|
||||
# NOTE(ameade): check for shares in the CG
|
||||
if self.db.count_shares_in_consistency_group(context, cg_id):
|
||||
msg = (_("Cannot delete a consistency group with shares"))
|
||||
raise exception.InvalidConsistencyGroup(reason=msg)
|
||||
|
||||
cg = self.db.consistency_group_update(
|
||||
context, cg_id, {'status': constants.STATUS_DELETING})
|
||||
|
||||
self.share_rpcapi.delete_consistency_group(context, cg)
|
||||
|
||||
@policy.wrap_check_policy('consistency_group')
|
||||
def update(self, context, cg, fields):
|
||||
return self.db.consistency_group_update(context, cg['id'], fields)
|
||||
|
||||
def get(self, context, cg_id):
|
||||
policy.check_policy(context, 'consistency_group', 'get')
|
||||
return self.db.consistency_group_get(context, cg_id)
|
||||
|
||||
def get_all(self, context, detailed=True, search_opts=None):
|
||||
policy.check_policy(context, 'consistency_group', 'get_all')
|
||||
|
||||
if search_opts is None:
|
||||
search_opts = {}
|
||||
|
||||
LOG.debug("Searching for consistency_groups by: %s",
|
||||
six.text_type(search_opts))
|
||||
|
||||
# Get filtered list of consistency_groups
|
||||
if context.is_admin and search_opts.get('all_tenants'):
|
||||
consistency_groups = self.db.consistency_group_get_all(
|
||||
context, detailed=detailed)
|
||||
else:
|
||||
consistency_groups = self.db.consistency_group_get_all_by_project(
|
||||
context, context.project_id, detailed=detailed)
|
||||
|
||||
return consistency_groups
|
||||
|
||||
def create_cgsnapshot(self, context, name=None, description=None,
|
||||
consistency_group_id=None):
|
||||
"""Create new cgsnapshot."""
|
||||
policy.check_policy(context, 'consistency_group', 'create_cgsnapshot')
|
||||
|
||||
options = {
|
||||
'consistency_group_id': consistency_group_id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'user_id': context.user_id,
|
||||
'project_id': context.project_id,
|
||||
'status': constants.STATUS_CREATING,
|
||||
}
|
||||
|
||||
cg = self.db.consistency_group_get(context, consistency_group_id)
|
||||
# Check status of CG, must be active
|
||||
if not cg['status'] == constants.STATUS_AVAILABLE:
|
||||
msg = (_("Consistency group status must be %s")
|
||||
% constants.STATUS_AVAILABLE)
|
||||
raise exception.InvalidConsistencyGroup(reason=msg)
|
||||
|
||||
# Create members for every share in the CG
|
||||
shares = self.db.share_get_all_by_consistency_group_id(
|
||||
context, consistency_group_id)
|
||||
|
||||
# Check status of all shares, they must be active in order to snap
|
||||
# the CG
|
||||
for s in shares:
|
||||
if not s['status'] == constants.STATUS_AVAILABLE:
|
||||
msg = (_("Share %(s)s in consistency group must have status "
|
||||
"of %(status)s in order to create a CG snapshot")
|
||||
% {"s": s['id'],
|
||||
"status": constants.STATUS_AVAILABLE})
|
||||
raise exception.InvalidConsistencyGroup(reason=msg)
|
||||
|
||||
snap = self.db.cgsnapshot_create(context, options)
|
||||
|
||||
try:
|
||||
members = []
|
||||
for s in shares:
|
||||
member_options = {
|
||||
'cgsnapshot_id': snap['id'],
|
||||
'user_id': context.user_id,
|
||||
'project_id': context.project_id,
|
||||
'status': constants.STATUS_CREATING,
|
||||
'size': s['size'],
|
||||
'share_proto': s['share_proto'],
|
||||
'share_type_id': s['share_type_id'],
|
||||
'share_id': s['id'],
|
||||
'share_instance_id': s.instance['id']
|
||||
}
|
||||
member = self.db.cgsnapshot_member_create(context,
|
||||
member_options)
|
||||
members.append(member)
|
||||
|
||||
# Cast to share manager
|
||||
self.share_rpcapi.create_cgsnapshot(context, snap, cg['host'])
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
# This will delete the snapshot and all of it's members
|
||||
self.db.cgsnapshot_destroy(context, snap['id'])
|
||||
|
||||
return snap
|
||||
|
||||
@policy.wrap_check_policy('consistency_group')
|
||||
def delete_cgsnapshot(self, context, snap):
|
||||
"""Delete consistency group snapshot."""
|
||||
|
||||
snap_id = snap['id']
|
||||
|
||||
cg = self.db.consistency_group_get(context,
|
||||
snap['consistency_group_id'])
|
||||
|
||||
statuses = (constants.STATUS_AVAILABLE, constants.STATUS_ERROR)
|
||||
if not snap['status'] in statuses:
|
||||
msg = (_("Consistency group snapshot status must be one of"
|
||||
" %(statuses)s")
|
||||
% {"statuses": statuses})
|
||||
raise exception.InvalidCGSnapshot(reason=msg)
|
||||
|
||||
self.db.cgsnapshot_update(context, snap_id,
|
||||
{'status': constants.STATUS_DELETING})
|
||||
|
||||
# Cast to share manager
|
||||
self.share_rpcapi.delete_cgsnapshot(context, snap, cg['host'])
|
||||
|
||||
@policy.wrap_check_policy('consistency_group')
|
||||
def update_cgsnapshot(self, context, cg, fields):
|
||||
return self.db.cgsnapshot_update(context, cg['id'], fields)
|
||||
|
||||
def get_cgsnapshot(self, context, snapshot_id):
|
||||
policy.check_policy(context, 'consistency_group', 'get_cgsnapshot')
|
||||
return self.db.cgsnapshot_get(context, snapshot_id)
|
||||
|
||||
def get_all_cgsnapshots(self, context, detailed=True, search_opts=None):
|
||||
policy.check_policy(context, 'consistency_group',
|
||||
'get_all_cgsnapshots')
|
||||
|
||||
if search_opts is None:
|
||||
search_opts = {}
|
||||
|
||||
LOG.debug("Searching for consistency group snapshots by: %s",
|
||||
six.text_type(search_opts))
|
||||
|
||||
# Get filtered list of consistency_groups
|
||||
if context.is_admin and search_opts.get('all_tenants'):
|
||||
cgsnapshots = self.db.cgsnapshot_get_all(
|
||||
context, detailed=detailed)
|
||||
else:
|
||||
cgsnapshots = self.db.cgsnapshot_get_all_by_project(
|
||||
context, context.project_id, detailed=detailed)
|
||||
|
||||
return cgsnapshots
|
||||
|
||||
def get_all_cgsnapshot_members(self, context, cgsnapshot_id):
|
||||
policy.check_policy(context, 'consistency_group', 'get_cgsnapshot')
|
||||
members = self.db.cgsnapshot_members_get_all(context,
|
||||
cgsnapshot_id)
|
||||
|
||||
return members
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
427
manila/tests/api/v1/test_cgsnapshots.py
Normal file
427
manila/tests/api/v1/test_cgsnapshots.py
Normal file
@ -0,0 +1,427 @@
|
||||
# Copyright 2015 Alex Meade
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
import six
|
||||
import webob
|
||||
|
||||
import manila.api.v1.cgsnapshots as cgs
|
||||
from manila.common import constants
|
||||
from manila import exception
|
||||
from manila import test
|
||||
from manila.tests.api import fakes
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class CGSnapshotApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(CGSnapshotApiTest, self).setUp()
|
||||
self.controller = cgs.CGSnapshotController()
|
||||
self.api_version = '1.5'
|
||||
self.request = fakes.HTTPRequest.blank('/consistency-groups',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
def _get_fake_cgsnapshot(self, **values):
|
||||
snap = {
|
||||
'id': 'fake_id',
|
||||
'user_id': 'fakeuser',
|
||||
'project_id': 'fakeproject',
|
||||
'status': constants.STATUS_CREATING,
|
||||
'name': None,
|
||||
'description': None,
|
||||
'consistency_group_id': None,
|
||||
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
|
||||
}
|
||||
|
||||
snap.update(**values)
|
||||
|
||||
expected_snap = copy.deepcopy(snap)
|
||||
del expected_snap['user_id']
|
||||
expected_snap['links'] = mock.ANY
|
||||
return snap, expected_snap
|
||||
|
||||
def _get_fake_simple_cgsnapshot(self, **values):
|
||||
snap = {
|
||||
'id': 'fake_id',
|
||||
'name': None,
|
||||
}
|
||||
|
||||
snap.update(**values)
|
||||
expected_snap = copy.deepcopy(snap)
|
||||
expected_snap['links'] = mock.ANY
|
||||
return snap, expected_snap
|
||||
|
||||
def _get_fake_cgsnapshot_member(self, **values):
|
||||
member = {
|
||||
'id': 'fake_id',
|
||||
'user_id': 'fakeuser',
|
||||
'project_id': 'fakeproject',
|
||||
'status': constants.STATUS_CREATING,
|
||||
'cgsnapshot_id': None,
|
||||
'share_proto': None,
|
||||
'share_type_id': None,
|
||||
'share_id': None,
|
||||
'size': None,
|
||||
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
|
||||
}
|
||||
|
||||
member.update(**values)
|
||||
|
||||
expected_member = copy.deepcopy(member)
|
||||
del expected_member['user_id']
|
||||
del expected_member['status']
|
||||
expected_member['share_protocol'] = member['share_proto']
|
||||
del expected_member['share_proto']
|
||||
return member, expected_member
|
||||
|
||||
def test_create_invalid_body(self):
|
||||
body = {"not_cg_snapshot": {}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_create_no_consistency_group_id(self):
|
||||
body = {"cgnapshot": {}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_create(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
fake_id = six.text_type(uuid.uuid4())
|
||||
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
|
||||
body = {"cgsnapshot": {"consistency_group_id": fake_id}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
|
||||
context, consistency_group_id=fake_id)
|
||||
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
|
||||
|
||||
def test_create_cg_does_not_exist(self):
|
||||
fake_id = six.text_type(uuid.uuid4())
|
||||
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
|
||||
mock.Mock(
|
||||
side_effect=exception.ConsistencyGroupNotFound(
|
||||
consistency_group_id=six.text_type(
|
||||
uuid.uuid4())
|
||||
)))
|
||||
|
||||
body = {"cgsnapshot": {"consistency_group_id": fake_id}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_create_cg_does_not_a_uuid(self):
|
||||
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
|
||||
mock.Mock(
|
||||
side_effect=exception.ConsistencyGroupNotFound(
|
||||
consistency_group_id='not_a_uuid'
|
||||
)))
|
||||
|
||||
body = {"cgsnapshot": {"consistency_group_id": "not_a_uuid"}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_create_invalid_cg(self):
|
||||
fake_id = six.text_type(uuid.uuid4())
|
||||
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
|
||||
mock.Mock(
|
||||
side_effect=exception.InvalidConsistencyGroup(
|
||||
reason='bad_status'
|
||||
)))
|
||||
|
||||
body = {"cgsnapshot": {"consistency_group_id": fake_id}}
|
||||
self.assertRaises(webob.exc.HTTPConflict, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_create_with_name(self):
|
||||
fake_name = 'fake_name'
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot(name=fake_name)
|
||||
fake_id = six.text_type(uuid.uuid4())
|
||||
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
|
||||
body = {"cgsnapshot": {"consistency_group_id": fake_id,
|
||||
"name": fake_name}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
|
||||
context, consistency_group_id=fake_id, name=fake_name)
|
||||
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
|
||||
|
||||
def test_create_with_description(self):
|
||||
fake_description = 'fake_description'
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot(
|
||||
description=fake_description)
|
||||
fake_id = six.text_type(uuid.uuid4())
|
||||
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
|
||||
body = {"cgsnapshot": {"consistency_group_id": fake_id,
|
||||
"description": fake_description}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
|
||||
context, consistency_group_id=fake_id,
|
||||
description=fake_description)
|
||||
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
|
||||
|
||||
def test_create_with_name_and_description(self):
|
||||
fake_name = 'fake_name'
|
||||
fake_description = 'fake_description'
|
||||
fake_id = six.text_type(uuid.uuid4())
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot(
|
||||
description=fake_description, name=fake_name)
|
||||
self.mock_object(self.controller.cg_api, 'create_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
|
||||
body = {"cgsnapshot": {"consistency_group_id": fake_id,
|
||||
"description": fake_description,
|
||||
"name": fake_name}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create_cgsnapshot.assert_called_once_with(
|
||||
context, consistency_group_id=fake_id, name=fake_name,
|
||||
description=fake_description)
|
||||
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
|
||||
|
||||
def test_update_with_name_and_description(self):
|
||||
fake_name = 'fake_name'
|
||||
fake_description = 'fake_description'
|
||||
fake_id = six.text_type(uuid.uuid4())
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot(
|
||||
description=fake_description, name=fake_name)
|
||||
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
self.mock_object(self.controller.cg_api, 'update_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
|
||||
body = {"cgsnapshot": {"description": fake_description,
|
||||
"name": fake_name}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.update(self.request, fake_id, body)
|
||||
|
||||
self.controller.cg_api.update_cgsnapshot.assert_called_once_with(
|
||||
context, fake_snap,
|
||||
dict(name=fake_name, description=fake_description))
|
||||
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
|
||||
|
||||
def test_update_snapshot_not_found(self):
|
||||
body = {"cgsnapshot": {}}
|
||||
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
|
||||
mock.Mock(side_effect=exception.NotFound))
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
|
||||
def test_update_invalid_body(self):
|
||||
body = {"not_cgsnapshot": {}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
|
||||
def test_update_invalid_body_invalid_field(self):
|
||||
body = {"cgsnapshot": {"unknown_field": ""}}
|
||||
exc = self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
self.assertTrue('unknown_field' in six.text_type(exc))
|
||||
|
||||
def test_update_invalid_body_readonly_field(self):
|
||||
body = {"cgsnapshot": {"created_at": []}}
|
||||
exc = self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
self.assertTrue('created_at' in six.text_type(exc))
|
||||
|
||||
def test_list_index(self):
|
||||
fake_snap, expected_snap = self._get_fake_simple_cgsnapshot()
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[fake_snap]))
|
||||
res_dict = self.controller.index(self.request)
|
||||
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
|
||||
|
||||
def test_list_index_no_cgs(self):
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[]))
|
||||
res_dict = self.controller.index(self.request)
|
||||
self.assertEqual([], res_dict['cgsnapshots'])
|
||||
|
||||
def test_list_index_with_limit(self):
|
||||
fake_snap, expected_snap = self._get_fake_simple_cgsnapshot()
|
||||
fake_snap2, expected_snap2 = self._get_fake_simple_cgsnapshot(
|
||||
id="fake_id2")
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[fake_snap, fake_snap2]))
|
||||
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
res_dict = self.controller.index(req)
|
||||
self.assertEqual(1, len(res_dict['cgsnapshots']))
|
||||
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
|
||||
|
||||
def test_list_index_with_limit_and_offset(self):
|
||||
fake_snap, expected_snap = self._get_fake_simple_cgsnapshot()
|
||||
fake_snap2, expected_snap2 = self._get_fake_simple_cgsnapshot(
|
||||
id="fake_id2")
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[fake_snap, fake_snap2]))
|
||||
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1&offset=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
res_dict = self.controller.index(req)
|
||||
|
||||
self.assertEqual(1, len(res_dict['cgsnapshots']))
|
||||
self.assertEqual([expected_snap2], res_dict['cgsnapshots'])
|
||||
|
||||
def test_list_detail(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[fake_snap]))
|
||||
res_dict = self.controller.detail(self.request)
|
||||
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
|
||||
|
||||
def test_list_detail_no_cgs(self):
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[]))
|
||||
res_dict = self.controller.detail(self.request)
|
||||
self.assertEqual([], res_dict['cgsnapshots'])
|
||||
|
||||
def test_list_detail_with_limit(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
fake_snap2, expected_snap2 = self._get_fake_cgsnapshot(
|
||||
id="fake_id2")
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[fake_snap, fake_snap2]))
|
||||
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
res_dict = self.controller.detail(req)
|
||||
self.assertEqual(1, len(res_dict['cgsnapshots']))
|
||||
self.assertEqual([expected_snap], res_dict['cgsnapshots'])
|
||||
|
||||
def test_list_detail_with_limit_and_offset(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
fake_snap2, expected_snap2 = self._get_fake_cgsnapshot(
|
||||
id="fake_id2")
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshots',
|
||||
mock.Mock(return_value=[fake_snap, fake_snap2]))
|
||||
req = fakes.HTTPRequest.blank('/cgsnapshots?limit=1&offset=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
res_dict = self.controller.detail(req)
|
||||
|
||||
self.assertEqual(1, len(res_dict['cgsnapshots']))
|
||||
self.assertEqual([expected_snap2], res_dict['cgsnapshots'])
|
||||
|
||||
def test_delete(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
self.mock_object(self.controller.cg_api, 'delete_cgsnapshot')
|
||||
|
||||
res = self.controller.delete(self.request, fake_snap['id'])
|
||||
|
||||
self.assertEqual(202, res.status_code)
|
||||
|
||||
def test_delete_not_found(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
|
||||
mock.Mock(side_effect=exception.NotFound))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
|
||||
self.request, fake_snap['id'])
|
||||
|
||||
def test_delete_in_conflicting_status(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
self.mock_object(self.controller.cg_api, 'delete_cgsnapshot',
|
||||
mock.Mock(
|
||||
side_effect=exception.InvalidCGSnapshot(
|
||||
reason='blah')))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
|
||||
self.request, fake_snap['id'])
|
||||
|
||||
def test_show(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
|
||||
mock.Mock(return_value=fake_snap))
|
||||
|
||||
res_dict = self.controller.show(self.request, fake_snap['id'])
|
||||
|
||||
self.assertEqual(expected_snap, res_dict['cgsnapshot'])
|
||||
|
||||
def test_show_cg_not_found(self):
|
||||
fake_snap, expected_snap = self._get_fake_cgsnapshot()
|
||||
self.mock_object(self.controller.cg_api, 'get_cgsnapshot',
|
||||
mock.Mock(side_effect=exception.NotFound))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
||||
self.request, fake_snap['id'])
|
||||
|
||||
def test_members_empty(self):
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
|
||||
mock.Mock(return_value=[]))
|
||||
res_dict = self.controller.members(self.request, 'fake_cg_id')
|
||||
self.assertEqual([], res_dict['cgsnapshot_members'])
|
||||
|
||||
def test_members(self):
|
||||
fake_member, expected_member = self._get_fake_cgsnapshot_member()
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
|
||||
mock.Mock(return_value=[fake_member]))
|
||||
res_dict = self.controller.members(self.request, 'fake_cg_id')
|
||||
self.assertEqual([expected_member], res_dict['cgsnapshot_members'])
|
||||
|
||||
def test_members_with_limit(self):
|
||||
fake_member, expected_member = self._get_fake_cgsnapshot_member()
|
||||
fake_member2, expected_member2 = self._get_fake_cgsnapshot_member(
|
||||
id="fake_id2")
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
|
||||
mock.Mock(return_value=[fake_member, fake_member2]))
|
||||
req = fakes.HTTPRequest.blank('/members?limit=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
res_dict = self.controller.members(req, 'fake_cg_id')
|
||||
self.assertEqual(1, len(res_dict['cgsnapshot_members']))
|
||||
|
||||
def test_members_with_limit_and_offset(self):
|
||||
fake_member, expected_member = self._get_fake_cgsnapshot_member()
|
||||
fake_member2, expected_member2 = self._get_fake_cgsnapshot_member(
|
||||
id="fake_id2")
|
||||
self.mock_object(self.controller.cg_api, 'get_all_cgsnapshot_members',
|
||||
mock.Mock(return_value=[fake_member, fake_member2]))
|
||||
req = fakes.HTTPRequest.blank('/members?limit=1&offset=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
res_dict = self.controller.members(req, 'fake_cg_id')
|
||||
|
||||
self.assertEqual(1, len(res_dict['cgsnapshot_members']))
|
||||
self.assertEqual([expected_member2], res_dict['cgsnapshot_members'])
|
505
manila/tests/api/v1/test_consistency_groups.py
Normal file
505
manila/tests/api/v1/test_consistency_groups.py
Normal file
@ -0,0 +1,505 @@
|
||||
# Copyright 2015 Alex Meade
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
import six
|
||||
import webob
|
||||
|
||||
import manila.api.v1.consistency_groups as cgs
|
||||
from manila.common import constants
|
||||
import manila.consistency_group.api as cg_api
|
||||
from manila import exception
|
||||
from manila.share import share_types
|
||||
from manila import test
|
||||
from manila.tests.api import fakes
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class CGApiTest(test.TestCase):
|
||||
"""Share Api Test."""
|
||||
def setUp(self):
|
||||
super(CGApiTest, self).setUp()
|
||||
self.controller = cgs.CGController()
|
||||
self.fake_share_type = {'id': six.text_type(uuid.uuid4())}
|
||||
self.api_version = '1.5'
|
||||
self.request = fakes.HTTPRequest.blank('/consistency-groups',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
def _get_fake_cg(self, **values):
|
||||
cg = {
|
||||
'id': 'fake_id',
|
||||
'user_id': 'fakeuser',
|
||||
'project_id': 'fakeproject',
|
||||
'status': constants.STATUS_CREATING,
|
||||
'name': None,
|
||||
'description': None,
|
||||
'host': None,
|
||||
'source_cgsnapshot_id': None,
|
||||
'share_network_id': None,
|
||||
'share_server_id': None,
|
||||
'share_types': [],
|
||||
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
|
||||
}
|
||||
|
||||
cg.update(**values)
|
||||
|
||||
expected_cg = copy.deepcopy(cg)
|
||||
del expected_cg['user_id']
|
||||
del expected_cg['share_server_id']
|
||||
expected_cg['links'] = mock.ANY
|
||||
expected_cg['share_types'] = [st['share_type_id']
|
||||
for st in cg.get('share_types')]
|
||||
return cg, expected_cg
|
||||
|
||||
def _get_fake_simple_cg(self, **values):
|
||||
cg = {
|
||||
'id': 'fake_id',
|
||||
'name': None,
|
||||
}
|
||||
|
||||
cg.update(**values)
|
||||
expected_cg = copy.deepcopy(cg)
|
||||
expected_cg['links'] = mock.ANY
|
||||
return cg, expected_cg
|
||||
|
||||
def test_cg_create(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=self.fake_share_type))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, share_type_ids=[self.fake_share_type['id']])
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_invalid_cgsnapshot_state(self):
|
||||
fake_snap_id = six.text_type(uuid.uuid4())
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(side_effect=exception.InvalidCGSnapshot(
|
||||
reason='bad status'
|
||||
)))
|
||||
|
||||
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
|
||||
self.assertRaises(webob.exc.HTTPConflict,
|
||||
self.controller.create, self.request, body)
|
||||
|
||||
def test_cg_create_no_default_share_type(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=None))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_with_name(self):
|
||||
fake_name = 'fake_name'
|
||||
fake_cg, expected_cg = self._get_fake_cg(name=fake_name)
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=self.fake_share_type))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {"name": fake_name}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, name=fake_name,
|
||||
share_type_ids=[self.fake_share_type['id']])
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_with_description(self):
|
||||
fake_description = 'fake_description'
|
||||
fake_cg, expected_cg = self._get_fake_cg(description=fake_description)
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=self.fake_share_type))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {"description": fake_description}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, description=fake_description,
|
||||
share_type_ids=[self.fake_share_type['id']])
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_with_share_types(self):
|
||||
fake_share_types = [{"share_type_id": self.fake_share_type['id']}]
|
||||
fake_cg, expected_cg = self._get_fake_cg(share_types=fake_share_types)
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {
|
||||
"share_types": [self.fake_share_type['id']]}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, share_type_ids=[self.fake_share_type['id']])
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_with_source_cgsnapshot_id(self):
|
||||
fake_snap_id = six.text_type(uuid.uuid4())
|
||||
fake_cg, expected_cg = self._get_fake_cg(
|
||||
source_cgsnapshot_id=fake_snap_id)
|
||||
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=self.fake_share_type))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {
|
||||
"source_cgsnapshot_id": fake_snap_id}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, source_cgsnapshot_id=fake_snap_id)
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_with_share_network_id(self):
|
||||
fake_net_id = six.text_type(uuid.uuid4())
|
||||
fake_cg, expected_cg = self._get_fake_cg(
|
||||
share_network_id=fake_net_id)
|
||||
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=self.fake_share_type))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {
|
||||
"share_network_id": fake_net_id}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, share_network_id=fake_net_id, share_type_ids=mock.ANY)
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_no_default_share_type_with_cgsnapshot(self):
|
||||
fake_snap_id = six.text_type(uuid.uuid4())
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=None))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {
|
||||
"source_cgsnapshot_id": fake_snap_id}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, source_cgsnapshot_id=fake_snap_id)
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_with_name_and_description(self):
|
||||
fake_name = 'fake_name'
|
||||
fake_description = 'fake_description'
|
||||
fake_cg, expected_cg = self._get_fake_cg(name=fake_name,
|
||||
description=fake_description)
|
||||
self.mock_object(share_types, 'get_default_share_type',
|
||||
mock.Mock(return_value=self.fake_share_type))
|
||||
self.mock_object(self.controller.cg_api, 'create',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {"name": fake_name,
|
||||
"description": fake_description}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.create(self.request, body)
|
||||
|
||||
self.controller.cg_api.create.assert_called_once_with(
|
||||
context, name=fake_name, description=fake_description,
|
||||
share_type_ids=[self.fake_share_type['id']])
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_create_invalid_body(self):
|
||||
body = {"not_consistency_group": {}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_invalid_body_share_types_and_source_cgsnapshot(self):
|
||||
body = {"consistency_group": {"share_types": [],
|
||||
"source_cgsnapshot_id": ""}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_source_cgsnapshot_not_in_available(self):
|
||||
fake_snap_id = six.text_type(uuid.uuid4())
|
||||
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
|
||||
self.mock_object(self.controller.cg_api, 'create', mock.Mock(
|
||||
side_effect=exception.InvalidCGSnapshot(reason='blah')))
|
||||
self.assertRaises(webob.exc.HTTPConflict, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_source_cgsnapshot_does_not_exist(self):
|
||||
fake_snap_id = six.text_type(uuid.uuid4())
|
||||
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
|
||||
self.mock_object(self.controller.cg_api, 'create', mock.Mock(
|
||||
side_effect=exception.CGSnapshotNotFound(
|
||||
cgsnapshot_id=fake_snap_id)))
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_source_cgsnapshot_not_a_uuid(self):
|
||||
fake_snap_id = "Not a uuid"
|
||||
body = {"consistency_group": {"source_cgsnapshot_id": fake_snap_id}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_share_network_id_not_a_uuid(self):
|
||||
fake_net_id = "Not a uuid"
|
||||
body = {"consistency_group": {"share_network_id": fake_net_id}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_invalid_body_share_types_not_a_list(self):
|
||||
body = {"consistency_group": {"share_types": ""}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_invalid_body_invalid_field(self):
|
||||
body = {"consistency_group": {"unknown_field": ""}}
|
||||
exc = self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.create,
|
||||
self.request, body)
|
||||
self.assertTrue('unknown_field' in six.text_type(exc))
|
||||
|
||||
def test_cg_create_with_invalid_share_types_field(self):
|
||||
body = {"consistency_group": {"share_types": 'iamastring'}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_create_with_invalid_share_types_field_not_uuids(self):
|
||||
body = {"consistency_group": {"share_types": ['iamastring']}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
self.request, body)
|
||||
|
||||
def test_cg_update_with_name_and_description(self):
|
||||
fake_name = 'fake_name'
|
||||
fake_description = 'fake_description'
|
||||
fake_cg, expected_cg = self._get_fake_cg(name=fake_name,
|
||||
description=fake_description)
|
||||
self.mock_object(self.controller.cg_api, 'get',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
self.mock_object(self.controller.cg_api, 'update',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
|
||||
body = {"consistency_group": {"name": fake_name,
|
||||
"description": fake_description}}
|
||||
context = self.request.environ['manila.context']
|
||||
res_dict = self.controller.update(self.request, fake_cg['id'], body)
|
||||
|
||||
self.controller.cg_api.update.assert_called_once_with(
|
||||
context, fake_cg,
|
||||
{"name": fake_name, "description": fake_description})
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_update_cg_not_found(self):
|
||||
body = {"consistency_group": {}}
|
||||
self.mock_object(self.controller.cg_api, 'get',
|
||||
mock.Mock(side_effect=exception.NotFound))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
|
||||
def test_cg_update_invalid_body(self):
|
||||
body = {"not_consistency_group": {}}
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
|
||||
def test_cg_update_invalid_body_invalid_field(self):
|
||||
body = {"consistency_group": {"unknown_field": ""}}
|
||||
exc = self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
self.assertTrue('unknown_field' in six.text_type(exc))
|
||||
|
||||
def test_cg_update_invalid_body_readonly_field(self):
|
||||
body = {"consistency_group": {"share_types": []}}
|
||||
exc = self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.update,
|
||||
self.request, 'fake_id', body)
|
||||
self.assertTrue('share_types' in six.text_type(exc))
|
||||
|
||||
def test_cg_list_index(self):
|
||||
fake_cg, expected_cg = self._get_fake_simple_cg()
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[fake_cg]))
|
||||
res_dict = self.controller.index(self.request)
|
||||
self.assertEqual([expected_cg], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_list_index_no_cgs(self):
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[]))
|
||||
res_dict = self.controller.index(self.request)
|
||||
self.assertEqual([], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_list_index_with_limit(self):
|
||||
fake_cg, expected_cg = self._get_fake_simple_cg()
|
||||
fake_cg2, expected_cg2 = self._get_fake_simple_cg(id="fake_id2")
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[fake_cg, fake_cg2]))
|
||||
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
res_dict = self.controller.index(req)
|
||||
self.assertEqual(1, len(res_dict['consistency_groups']))
|
||||
self.assertEqual([expected_cg], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_list_index_with_limit_and_offset(self):
|
||||
fake_cg, expected_cg = self._get_fake_simple_cg()
|
||||
fake_cg2, expected_cg2 = self._get_fake_simple_cg(id="fake_id2")
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[fake_cg, fake_cg2]))
|
||||
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1&offset=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
res_dict = self.controller.index(req)
|
||||
|
||||
self.assertEqual(1, len(res_dict['consistency_groups']))
|
||||
self.assertEqual([expected_cg2], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_list_detail(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[fake_cg]))
|
||||
|
||||
res_dict = self.controller.detail(self.request)
|
||||
|
||||
self.assertEqual([expected_cg], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_list_detail_no_cgs(self):
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[]))
|
||||
|
||||
res_dict = self.controller.detail(self.request)
|
||||
|
||||
self.assertEqual([], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_list_detail_with_limit(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
fake_cg2, expected_cg2 = self._get_fake_cg(id="fake_id2")
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[fake_cg, fake_cg2]))
|
||||
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
res_dict = self.controller.detail(req)
|
||||
|
||||
self.assertEqual(1, len(res_dict['consistency_groups']))
|
||||
self.assertEqual([expected_cg], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_list_detail_with_limit_and_offset(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
fake_cg2, expected_cg2 = self._get_fake_cg(id="fake_id2")
|
||||
self.mock_object(cg_api.API, 'get_all',
|
||||
mock.Mock(return_value=[fake_cg, fake_cg2]))
|
||||
req = fakes.HTTPRequest.blank('/consistency_groups?limit=1&offset=1',
|
||||
version=self.api_version,
|
||||
experimental=True)
|
||||
|
||||
res_dict = self.controller.detail(req)
|
||||
|
||||
self.assertEqual(1, len(res_dict['consistency_groups']))
|
||||
self.assertEqual([expected_cg2], res_dict['consistency_groups'])
|
||||
|
||||
def test_cg_delete(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(cg_api.API, 'get',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
self.mock_object(cg_api.API, 'delete')
|
||||
|
||||
res = self.controller.delete(self.request, fake_cg['id'])
|
||||
|
||||
self.assertEqual(202, res.status_code)
|
||||
|
||||
def test_cg_delete_cg_not_found(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(cg_api.API, 'get',
|
||||
mock.Mock(side_effect=exception.NotFound))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
|
||||
self.request, fake_cg['id'])
|
||||
|
||||
def test_cg_delete_in_conflicting_status(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(cg_api.API, 'get',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
self.mock_object(cg_api.API, 'delete', mock.Mock(
|
||||
side_effect=exception.InvalidConsistencyGroup(reason='blah')))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPConflict, self.controller.delete,
|
||||
self.request, fake_cg['id'])
|
||||
|
||||
def test_cg_show(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(cg_api.API, 'get',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/consistency_groups/%s' % fake_cg['id'],
|
||||
version=self.api_version, experimental=True)
|
||||
|
||||
res_dict = self.controller.show(req, fake_cg['id'])
|
||||
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_show_as_admin(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
expected_cg['share_server_id'] = None
|
||||
self.mock_object(cg_api.API, 'get',
|
||||
mock.Mock(return_value=fake_cg))
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/consistency_groups/%s' % fake_cg['id'],
|
||||
version=self.api_version, experimental=True)
|
||||
admin_context = req.environ['manila.context'].elevated()
|
||||
req.environ['manila.context'] = admin_context
|
||||
|
||||
res_dict = self.controller.show(req, fake_cg['id'])
|
||||
|
||||
self.assertEqual(expected_cg, res_dict['consistency_group'])
|
||||
|
||||
def test_cg_show_cg_not_found(self):
|
||||
fake_cg, expected_cg = self._get_fake_cg()
|
||||
self.mock_object(cg_api.API, 'get',
|
||||
mock.Mock(side_effect=exception.NotFound))
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/consistency_groups/%s' % fake_cg['id'],
|
||||
version=self.api_version, experimental=True)
|
||||
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
||||
req, fake_cg['id'])
|
@ -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,
|
||||
|
@ -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'}
|
||||
|
0
manila/tests/consistency_group/__init__.py
Normal file
0
manila/tests/consistency_group/__init__.py
Normal file
1293
manila/tests/consistency_group/test_api.py
Normal file
1293
manila/tests/consistency_group/test_api.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user