diff --git a/etc/manila/api-paste.ini b/etc/manila/api-paste.ini index 41e739d0d0..4e0f32eb0b 100644 --- a/etc/manila/api-paste.ini +++ b/etc/manila/api-paste.ini @@ -6,7 +6,7 @@ use = call:manila.api:root_app_factory /: apiversions /v1: openstack_share_api -/v2: openstack_share_api +/v2: openstack_share_api_v2 [composite:openstack_share_api] use = call:manila.api.middleware.auth:pipeline_factory @@ -14,6 +14,12 @@ noauth = cors faultwrap ssl sizelimit noauth api keystone = cors faultwrap ssl sizelimit authtoken keystonecontext api keystone_nolimit = cors faultwrap ssl sizelimit authtoken keystonecontext api +[composite:openstack_share_api_v2] +use = call:manila.api.middleware.auth:pipeline_factory +noauth = cors faultwrap ssl sizelimit noauth apiv2 +keystone = cors faultwrap ssl sizelimit authtoken keystonecontext apiv2 +keystone_nolimit = cors faultwrap ssl sizelimit authtoken keystonecontext apiv2 + [filter:faultwrap] paste.filter_factory = manila.api.middleware.fault:FaultWrapper.factory @@ -29,6 +35,9 @@ paste.filter_factory = oslo_middleware.ssl:SSLMiddleware.factory [app:api] paste.app_factory = manila.api.v1.router:APIRouter.factory +[app:apiv2] +paste.app_factory = manila.api.v2.router:APIRouter.factory + [pipeline:apiversions] pipeline = cors faultwrap osshareversionapp diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index 93364dc77b..1c5ba3c22c 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -52,6 +52,7 @@ REST_API_VERSION_HISTORY = """ * 2.4 - Consistency Group support * 2.5 - Share Migration admin API * 2.6 - Return share_type UUID instead of name in Share API + * 2.7 - Rename old extension-like API URLs to core-API-like """ @@ -59,7 +60,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 = "2.0" -_MAX_API_VERSION = "2.6" +_MAX_API_VERSION = "2.7" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index c690ec6e18..08f5bdb882 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -61,3 +61,7 @@ user documentation. --- Return share_type UUID instead of name in Share API and add share_type_name field. + +2.7 +--- + Rename old extension-like API URLs to core-API-like. diff --git a/manila/api/openstack/wsgi.py b/manila/api/openstack/wsgi.py index 1c0a64443b..090bfa34db 100644 --- a/manila/api/openstack/wsgi.py +++ b/manila/api/openstack/wsgi.py @@ -1199,12 +1199,12 @@ class AdminActionsMixin(object): raise webob.exc.HTTPBadRequest(explanation=expl) return update - @action('os-reset_status') @Controller.authorize('reset_status') def _reset_status(self, req, id, body): """Reset status on the resource.""" context = req.environ['manila.context'] - update = self.validate_update(body['os-reset_status']) + update = self.validate_update( + body.get('reset_status', body.get('os-reset_status'))) msg = "Updating %(resource)s '%(id)s' with '%(update)r'" LOG.debug(msg, {'resource': self.resource_name, 'id': id, 'update': update}) @@ -1214,7 +1214,6 @@ class AdminActionsMixin(object): raise webob.exc.HTTPNotFound(six.text_type(e)) return webob.Response(status_int=202) - @action('os-force_delete') @Controller.authorize('force_delete') def _force_delete(self, req, id, body): """Delete a resource, bypassing the check for status.""" diff --git a/manila/api/v1/router.py b/manila/api/v1/router.py index f57c592aa8..3e16b5c1d5 100644 --- a/manila/api/v1/router.py +++ b/manila/api/v1/router.py @@ -16,32 +16,30 @@ # under the License. """ -WSGI middleware for OpenStack Share API. +WSGI middleware for OpenStack Share API v1. """ from oslo_log import log from manila.api import extensions import manila.api.openstack -from manila.api.v1 import availability_zones -from manila.api.v1 import cgsnapshots -from manila.api.v1 import consistency_groups from manila.api.v1 import limits -from manila.api.v1 import quota_class_sets -from manila.api.v1 import quota_sets from manila.api.v1 import scheduler_stats from manila.api.v1 import security_service -from manila.api.v1 import services from manila.api.v1 import share_instances from manila.api.v1 import share_manage from manila.api.v1 import share_metadata from manila.api.v1 import share_networks from manila.api.v1 import share_servers from manila.api.v1 import share_snapshots -from manila.api.v1 import share_types from manila.api.v1 import share_types_extra_specs from manila.api.v1 import share_unmanage from manila.api.v1 import shares +from manila.api.v2 import availability_zones +from manila.api.v2 import quota_class_sets +from manila.api.v2 import quota_sets +from manila.api.v2 import services +from manila.api.v2 import share_types from manila.api import versions LOG = log.getLogger(__name__) @@ -64,46 +62,35 @@ class APIRouter(manila.api.openstack.APIRouter): mapper.redirect("", "/") self.resources["availability_zones"] = ( - availability_zones.create_resource()) + availability_zones.create_resource_legacy()) mapper.resource("availability-zone", - # TODO(vponomaryov): rename 'os-availability-zone' to - # 'availability-zones' when API urls rename happens. "os-availability-zone", controller=self.resources["availability_zones"]) - self.resources["services"] = services.create_resource() + self.resources["services"] = services.create_resource_legacy() mapper.resource("service", - # TODO(vponomaryov): rename 'os-services' to - # 'services' when API urls rename happens. "os-services", controller=self.resources["services"]) - self.resources["quota_sets"] = quota_sets.create_resource() + self.resources["quota_sets"] = quota_sets.create_resource_legacy() mapper.resource("quota-set", - # TODO(vponomaryov): rename 'os-quota-sets' to - # 'quota-sets' when API urls rename happens. "os-quota-sets", controller=self.resources["quota_sets"], member={'defaults': 'GET'}) - self.resources["quota_class_sets"] = quota_class_sets.create_resource() + self.resources["quota_class_sets"] = ( + quota_class_sets.create_resource_legacy()) mapper.resource("quota-class-set", - # TODO(vponomaryov): rename 'os-quota-class-sets' to - # 'quota-class-sets' when API urls rename happens. "os-quota-class-sets", controller=self.resources["quota_class_sets"]) self.resources["share_manage"] = share_manage.create_resource() mapper.resource("share_manage", - # TODO(vponomaryov): remove it when it is ported - # to shares controller. "os-share-manage", controller=self.resources["share_manage"]) self.resources["share_unmanage"] = share_unmanage.create_resource() mapper.resource("share_unmanage", - # TODO(vponomaryov): remove it when it is ported - # to shares controller. "os-share-unmanage", controller=self.resources["share_unmanage"], member={'unmanage': 'POST'}) @@ -196,20 +183,3 @@ class APIRouter(manila.api.openstack.APIRouter): controller=self.resources['scheduler_stats'], action='pools_detail', conditions={'method': ['GET']}) - - self.resources['consistency-groups'] = ( - consistency_groups.create_resource()) - mapper.resource('consistency-group', 'consistency-groups', - controller=self.resources['consistency-groups'], - collection={'detail': 'GET'}) - mapper.connect('consistency-groups', - '/{project_id}/consistency-groups/{id}/action', - controller=self.resources['consistency-groups'], - action='action', - conditions={"action": ['POST']}) - - self.resources['cgsnapshots'] = cgsnapshots.create_resource() - mapper.resource('cgsnapshot', 'cgsnapshots', - controller=self.resources['cgsnapshots'], - collection={'detail': 'GET'}, - member={'members': 'GET', 'action': 'POST'}) diff --git a/manila/api/v1/share_instances.py b/manila/api/v1/share_instances.py index 86991fd3a7..ed220e8bb2 100644 --- a/manila/api/v1/share_instances.py +++ b/manila/api/v1/share_instances.py @@ -41,6 +41,26 @@ class ShareInstancesController(wsgi.Controller, wsgi.AdminActionsMixin): def _delete(self, *args, **kwargs): return self.share_api.delete_instance(*args, **kwargs) + @wsgi.Controller.api_version('2.3', '2.6') + @wsgi.action('os-reset_status') + def instance_reset_status_legacy(self, req, id, body): + return self._reset_status(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('reset_status') + def instance_reset_status(self, req, id, body): + return self._reset_status(req, id, body) + + @wsgi.Controller.api_version('2.3', '2.6') + @wsgi.action('os-force_delete') + def instance_force_delete_legacy(self, req, id, body): + return self._force_delete(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('force_delete') + def instance_force_delete(self, req, id, body): + return self._force_delete(req, id, body) + @wsgi.Controller.api_version("2.3") @wsgi.Controller.authorize def index(self, req): @@ -74,18 +94,6 @@ class ShareInstancesController(wsgi.Controller, wsgi.AdminActionsMixin): view = instance_view.ViewBuilder() return view.detail_list(req, share.instances) - @wsgi.Controller.api_version('2.3') - @wsgi.action('os-reset_status') - @wsgi.response(202) - def share_instance_reset_status(self, req, id, body): - super(self.__class__, self)._reset_status(req, id, body) - - @wsgi.Controller.api_version('2.3') - @wsgi.action('os-force_delete') - @wsgi.response(202) - def share_instance_force_delete(self, req, id, body): - super(self.__class__, self)._force_delete(req, id, body) - def create_resource(): return wsgi.Resource(ShareInstancesController()) diff --git a/manila/api/v1/share_manage.py b/manila/api/v1/share_manage.py index f9ae78c566..da032ecc7d 100644 --- a/manila/api/v1/share_manage.py +++ b/manila/api/v1/share_manage.py @@ -25,20 +25,10 @@ from manila.share import utils as share_utils from manila import utils -class ShareManageController(wsgi.Controller): - """Allows existing share to be 'managed' by Manila.""" - - resource_name = "share" - _view_builder_class = share_views.ViewBuilder - - def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - self.share_api = share.API() +class ShareManageMixin(object): @wsgi.Controller.authorize('manage') - def create(self, req, body): - # TODO(vponomaryov): move it to shares controller. - + def _manage(self, req, body): context = req.environ['manila.context'] share_data = self._validate_manage_parameters(context, body) @@ -118,5 +108,25 @@ class ShareManageController(wsgi.Controller): raise exc.HTTPNotFound(explanation=six.text_type(e)) +class ShareManageController(ShareManageMixin, wsgi.Controller): + """Allows existing share to be 'managed' by Manila.""" + + resource_name = "share" + _view_builder_class = share_views.ViewBuilder + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.share_api = share.API() + + @wsgi.Controller.api_version('1.0', '2.6') + def create(self, req, body): + """Legacy method for 'manage share' operation. + + Should be removed when minimum API version becomes equal to or + greater than v2.7 + """ + return self._manage(req, body) + + def create_resource(): return wsgi.Resource(ShareManageController()) diff --git a/manila/api/v1/share_snapshots.py b/manila/api/v1/share_snapshots.py index 2666078c67..c7c4dbf76a 100644 --- a/manila/api/v1/share_snapshots.py +++ b/manila/api/v1/share_snapshots.py @@ -50,15 +50,25 @@ class ShareSnapshotsController(wsgi.Controller, wsgi.AdminActionsMixin): def _delete(self, *args, **kwargs): return self.share_api.delete_snapshot(*args, **kwargs) + @wsgi.Controller.api_version('1.0', '2.6') @wsgi.action('os-reset_status') - @wsgi.response(202) - def share_snapshot_reset_status(self, req, id, body): - super(self.__class__, self)._reset_status(req, id, body) + def snapshot_reset_status_legacy(self, req, id, body): + return self._reset_status(req, id, body) + @wsgi.Controller.api_version('2.7') + @wsgi.action('reset_status') + def snapshot_reset_status(self, req, id, body): + return self._reset_status(req, id, body) + + @wsgi.Controller.api_version('1.0', '2.6') @wsgi.action('os-force_delete') - @wsgi.response(202) - def share_snapshot_force_delete(self, req, id, body): - super(self.__class__, self)._force_delete(req, id, body) + def snapshot_force_delete_legacy(self, req, id, body): + return self._force_delete(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('force_delete') + def snapshot_force_delete(self, req, id, body): + return self._force_delete(req, id, body) def show(self, req, id): """Return data about the given snapshot.""" diff --git a/manila/api/v1/share_unmanage.py b/manila/api/v1/share_unmanage.py index a840ef6c06..af866c6f54 100644 --- a/manila/api/v1/share_unmanage.py +++ b/manila/api/v1/share_unmanage.py @@ -26,20 +26,11 @@ from manila import share LOG = log.getLogger(__name__) -class ShareUnmanageController(wsgi.Controller): - """The Unmanage API controller for the OpenStack API.""" +class ShareUnmanageMixin(object): - resource_name = "share" - - def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - self.share_api = share.API() - - @wsgi.action("unmanage") - @wsgi.Controller.authorize - def unmanage(self, req, id): + @wsgi.Controller.authorize("unmanage") + def _unmanage(self, req, id, body=None): """Unmanage a share.""" - # TODO(vponomaryov): move it to shares controller as 'unmanage' action. context = req.environ['manila.context'] LOG.info(_LI("Unmanage share with id: %s"), id, context=context) @@ -72,5 +63,19 @@ class ShareUnmanageController(wsgi.Controller): return webob.Response(status_int=202) +class ShareUnmanageController(ShareUnmanageMixin, wsgi.Controller): + """The Unmanage API controller for the OpenStack API.""" + + resource_name = "share" + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.share_api = share.API() + + @wsgi.Controller.api_version('1.0', '2.6') + def unmanage(self, req, id): + return self._unmanage(req, id) + + def create_resource(): return wsgi.Resource(ShareUnmanageController()) diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index 2ce5d7c9fd..b424ea84ed 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -38,15 +38,8 @@ from manila.share import share_types LOG = log.getLogger(__name__) -class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): - """The Shares API controller for the OpenStack API.""" - - resource_name = 'share' - _view_builder_class = share_views.ViewBuilder - - def __init__(self): - super(ShareController, self).__init__() - self.share_api = share.API() +class ShareMixin(object): + """Mixin class for Share API Controllers.""" def _update(self, *args, **kwargs): db.share_update(*args, **kwargs) @@ -60,16 +53,6 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): def _migrate(self, *args, **kwargs): return self.share_api.migrate_share(*args, **kwargs) - @wsgi.action('os-reset_status') - @wsgi.response(202) - def share_reset_status(self, req, id, body): - super(self.__class__, self)._reset_status(req, id, body) - - @wsgi.action('os-force_delete') - @wsgi.response(202) - def share_force_delete(self, req, id, body): - super(self.__class__, self)._force_delete(req, id, body) - def show(self, req, id): """Return data about the given share.""" context = req.environ['manila.context'] @@ -113,9 +96,7 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): return webob.Response(status_int=202) - @wsgi.Controller.api_version("2.5", None, True) - @wsgi.action("os-migrate_share") - def migrate_share(self, req, id, body): + def _migrate_share(self, req, id, body): """Migrate a share to the specified host.""" context = req.environ['manila.context'] try: @@ -123,7 +104,7 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): except exception.NotFound: msg = _("Share %s not found.") % id raise exc.HTTPNotFound(explanation=msg) - params = body['os-migrate_share'] + params = body.get('migrate_share', body.get('os-migrate_share')) try: host = params['host'] except KeyError: @@ -229,12 +210,7 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): share.update(update_dict) return self._view_builder.detail(req, share) - @wsgi.Controller.api_version("2.4") def create(self, req, body): - return self._create(req, body) - - @wsgi.Controller.api_version("1.0", "2.3") # noqa - def create(self, req, body): # pylint: disable=E0102 # Remove consistency group attributes body.get('share', {}).pop('consistency_group_id', None) share = self._create(req, body) @@ -400,11 +376,10 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): except ValueError: raise webob.exc.HTTPBadRequest(explanation=exc_str) - @wsgi.action('os-allow_access') def _allow_access(self, req, id, body): """Add share access rule.""" context = req.environ['manila.context'] - access_data = body['os-allow_access'] + access_data = body.get('allow_access', body.get('os-allow_access')) share = self.share_api.get(context, id) access_type = access_data['access_type'] @@ -427,12 +402,12 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): raise webob.exc.HTTPBadRequest(explanation=e.msg) return {'access': access} - @wsgi.action('os-deny_access') def _deny_access(self, req, id, body): """Remove share access rule.""" context = req.environ['manila.context'] - access_id = body['os-deny_access']['access_id'] + access_id = body.get( + 'deny_access', body.get('os-deny_access'))['access_id'] try: access = self.share_api.access_get(context, access_id) @@ -444,7 +419,6 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): self.share_api.deny_access(context, share, access) return webob.Response(status_int=202) - @wsgi.action('os-access_list') def _access_list(self, req, id, body): """list share access rules.""" context = req.environ['manila.context'] @@ -453,7 +427,6 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): access_list = self.share_api.access_get_all(context, share) return {'access_list': access_list} - @wsgi.action('os-extend') def _extend(self, req, id, body): """Extend size of a share.""" context = req.environ['manila.context'] @@ -469,7 +442,6 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): return webob.Response(status_int=202) - @wsgi.action('os-shrink') def _shrink(self, req, id, body): """Shrink size of a share.""" context = req.environ['manila.context'] @@ -490,7 +462,7 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): raise webob.exc.HTTPNotFound(explanation=six.text_type(e)) try: - size = int(body[action]['new_size']) + size = int(body.get(action, action.split('os-')[-1])['new_size']) except (KeyError, ValueError, TypeError): msg = _("New share size must be specified as an integer.") raise webob.exc.HTTPBadRequest(explanation=msg) @@ -498,5 +470,50 @@ class ShareController(wsgi.Controller, wsgi.AdminActionsMixin): return share, size +class ShareController(wsgi.Controller, ShareMixin, wsgi.AdminActionsMixin): + """The Shares API v1 controller for the OpenStack API.""" + resource_name = 'share' + _view_builder_class = share_views.ViewBuilder + + def __init__(self): + super(self.__class__, self).__init__() + self.share_api = share.API() + + @wsgi.action('os-reset_status') + def share_reset_status(self, req, id, body): + """Reset status of a share.""" + return self._reset_status(req, id, body) + + @wsgi.action('os-force_delete') + def share_force_delete(self, req, id, body): + """Delete a share, bypassing the check for status.""" + return self._force_delete(req, id, body) + + @wsgi.action('os-allow_access') + def allow_access(self, req, id, body): + """Add share access rule.""" + return self._allow_access(req, id, body) + + @wsgi.action('os-deny_access') + def deny_access(self, req, id, body): + """Remove share access rule.""" + return self._deny_access(req, id, body) + + @wsgi.action('os-access_list') + def access_list(self, req, id, body): + """List share access rules.""" + return self._access_list(req, id, body) + + @wsgi.action('os-extend') + def extend(self, req, id, body): + """Extend size of a share.""" + return self._extend(req, id, body) + + @wsgi.action('os-shrink') + def shrink(self, req, id, body): + """Shrink size of a share.""" + return self._shrink(req, id, body) + + def create_resource(): return wsgi.Resource(ShareController()) diff --git a/manila/api/v2/__init__.py b/manila/api/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/api/v1/availability_zones.py b/manila/api/v2/availability_zones.py similarity index 54% rename from manila/api/v1/availability_zones.py rename to manila/api/v2/availability_zones.py index 6126d513ba..a781be7d04 100644 --- a/manila/api/v1/availability_zones.py +++ b/manila/api/v2/availability_zones.py @@ -18,21 +18,50 @@ from manila.api.views import availability_zones as availability_zones_views from manila import db -class AvailabilityZoneController(wsgi.Controller): - """The Availability Zone API controller for the OpenStack API.""" +class AvailabilityZoneMixin(object): + """The Availability Zone API controller common logic. + + Mixin class that should be inherited by Availability Zone API controllers, + which are used for different API URLs and microversions. + """ resource_name = "availability_zone" _view_builder_class = availability_zones_views.ViewBuilder - @wsgi.Controller.authorize - def index(self, req): - return self._index(req) - + @wsgi.Controller.authorize("index") def _index(self, req): """Describe all known availability zones.""" views = db.availability_zone_get_all(req.environ['manila.context']) return self._view_builder.detail_list(views) +class AvailabilityZoneControllerLegacy(AvailabilityZoneMixin, wsgi.Controller): + """Deprecated Availability Zone API controller. + + Used by legacy API v1 and v2 microversions from 2.0 to 2.6. + Registered under deprecated API URL 'os-availability-zone'. + """ + + @wsgi.Controller.api_version('1.0', '2.6') + def index(self, req): + return self._index(req) + + +class AvailabilityZoneController(AvailabilityZoneMixin, wsgi.Controller): + """Availability Zone API controller. + + Used only by API v2 starting from microversion 2.7. + Registered under API URL 'availability-zones'. + """ + + @wsgi.Controller.api_version('2.7') + def index(self, req): + return self._index(req) + + +def create_resource_legacy(): + return wsgi.Resource(AvailabilityZoneControllerLegacy()) + + def create_resource(): return wsgi.Resource(AvailabilityZoneController()) diff --git a/manila/api/v1/cgsnapshots.py b/manila/api/v2/cgsnapshots.py similarity index 92% rename from manila/api/v1/cgsnapshots.py rename to manila/api/v2/cgsnapshots.py index 9e00a3fc15..780ae00d85 100644 --- a/manila/api/v1/cgsnapshots.py +++ b/manila/api/v2/cgsnapshots.py @@ -211,17 +211,25 @@ class CGSnapshotController(wsgi.Controller, wsgi.AdminActionsMixin): def _delete(self, context, resource, force=True): db.cgsnapshot_destroy(context.elevated(), resource['id']) - @wsgi.Controller.api_version('2.4', experimental=True) + @wsgi.Controller.api_version('2.4', '2.6', experimental=True) @wsgi.action('os-reset_status') - @wsgi.response(202) - def cgsnapshot_reset_status(self, req, id, body): - super(self.__class__, self)._reset_status(req, id, body) + def cgsnapshot_reset_status_legacy(self, req, id, body): + return self._reset_status(req, id, body) - @wsgi.Controller.api_version('2.4', experimental=True) + @wsgi.Controller.api_version('2.7', experimental=True) + @wsgi.action('reset_status') + def cgsnapshot_reset_status(self, req, id, body): + return self._reset_status(req, id, body) + + @wsgi.Controller.api_version('2.4', '2.6', experimental=True) @wsgi.action('os-force_delete') - @wsgi.response(202) + def cgsnapshot_force_delete_legacy(self, req, id, body): + return self._force_delete(req, id, body) + + @wsgi.Controller.api_version('2.7', experimental=True) + @wsgi.action('force_delete') def cgsnapshot_force_delete(self, req, id, body): - super(self.__class__, self)._force_delete(req, id, body) + return self._force_delete(req, id, body) def create_resource(): diff --git a/manila/api/v1/consistency_groups.py b/manila/api/v2/consistency_groups.py similarity index 93% rename from manila/api/v1/consistency_groups.py rename to manila/api/v2/consistency_groups.py index 154e44143d..0ef6073a00 100644 --- a/manila/api/v1/consistency_groups.py +++ b/manila/api/v2/consistency_groups.py @@ -224,17 +224,25 @@ class CGController(wsgi.Controller, wsgi.AdminActionsMixin): def _delete(self, context, resource, force=True): db.consistency_group_destroy(context.elevated(), resource['id']) - @wsgi.Controller.api_version('2.4', experimental=True) + @wsgi.Controller.api_version('2.4', '2.6', experimental=True) @wsgi.action('os-reset_status') - @wsgi.response(202) - def cg_reset_status(self, req, id, body): - super(self.__class__, self)._reset_status(req, id, body) + def cg_reset_status_legacy(self, req, id, body): + return self._reset_status(req, id, body) - @wsgi.Controller.api_version('2.4', experimental=True) + @wsgi.Controller.api_version('2.7', experimental=True) + @wsgi.action('reset_status') + def cg_reset_status(self, req, id, body): + return self._reset_status(req, id, body) + + @wsgi.Controller.api_version('2.4', '2.6', experimental=True) @wsgi.action('os-force_delete') - @wsgi.response(202) + def cg_force_delete_legacy(self, req, id, body): + return self._force_delete(req, id, body) + + @wsgi.Controller.api_version('2.7', experimental=True) + @wsgi.action('force_delete') def cg_force_delete(self, req, id, body): - super(self.__class__, self)._force_delete(req, id, body) + return self._force_delete(req, id, body) def create_resource(): diff --git a/manila/api/v1/quota_class_sets.py b/manila/api/v2/quota_class_sets.py similarity index 60% rename from manila/api/v1/quota_class_sets.py rename to manila/api/v2/quota_class_sets.py index ca9cabda2a..0466bc9bde 100644 --- a/manila/api/v1/quota_class_sets.py +++ b/manila/api/v2/quota_class_sets.py @@ -25,13 +25,18 @@ from manila import quota QUOTAS = quota.QUOTAS -class QuotaClassSetsController(wsgi.Controller): +class QuotaClassSetsMixin(object): + """The Quota Class Sets API controller common logic. + + Mixin class that should be inherited by Quota Class Sets API controllers, + which are used for different API URLs and microversions. + """ resource_name = "quota_class_set" _view_builder_class = quota_class_sets_views.ViewBuilder - @wsgi.Controller.authorize - def show(self, req, id): + @wsgi.Controller.authorize("show") + def _show(self, req, id): context = req.environ['manila.context'] try: db.authorize_quota_class_context(context, id) @@ -41,8 +46,8 @@ class QuotaClassSetsController(wsgi.Controller): return self._view_builder.detail_list( QUOTAS.get_class_quotas(context, id), id) - @wsgi.Controller.authorize - def update(self, req, id, body): + @wsgi.Controller.authorize("update") + def _update(self, req, id, body): context = req.environ['manila.context'] quota_class = id for key in body.get(self.resource_name, {}).keys(): @@ -58,5 +63,41 @@ class QuotaClassSetsController(wsgi.Controller): QUOTAS.get_class_quotas(context, quota_class)) +class QuotaClassSetsControllerLegacy(QuotaClassSetsMixin, wsgi.Controller): + """Deprecated Quota Class Sets API controller. + + Used by legacy API v1 and v2 microversions from 2.0 to 2.6. + Registered under deprecated API URL 'os-quota-class-sets'. + """ + + @wsgi.Controller.api_version('1.0', '2.6') + def show(self, req, id): + return self._show(req, id) + + @wsgi.Controller.api_version('1.0', '2.6') + def update(self, req, id, body): + return self._update(req, id, body) + + +class QuotaClassSetsController(QuotaClassSetsMixin, wsgi.Controller): + """Quota Class Sets API controller. + + Used only by API v2 starting from microversion 2.7. + Registered under API URL 'quota-class-sets'. + """ + + @wsgi.Controller.api_version('2.7') + def show(self, req, id): + return self._show(req, id) + + @wsgi.Controller.api_version('2.7') + def update(self, req, id, body): + return self._update(req, id, body) + + +def create_resource_legacy(): + return wsgi.Resource(QuotaClassSetsControllerLegacy()) + + def create_resource(): return wsgi.Resource(QuotaClassSetsController()) diff --git a/manila/api/v1/quota_sets.py b/manila/api/v2/quota_sets.py similarity index 80% rename from manila/api/v1/quota_sets.py rename to manila/api/v2/quota_sets.py index dc2372e0c4..bd9e38a151 100644 --- a/manila/api/v1/quota_sets.py +++ b/manila/api/v2/quota_sets.py @@ -31,8 +31,12 @@ LOG = log.getLogger(__name__) NON_QUOTA_KEYS = ('tenant_id', 'id', 'force') -class QuotaSetsController(wsgi.Controller): - """The Quota Sets API controller for the OpenStack API.""" +class QuotaSetsMixin(object): + """The Quota Sets API controller common logic. + + Mixin class that should be inherited by Quota Sets API controllers, + which are used for different API URLs and microversions. + """ resource_name = "quota_set" _view_builder_class = quota_sets_views.ViewBuilder @@ -61,8 +65,8 @@ class QuotaSetsController(wsgi.Controller): return values return dict((k, v['limit']) for k, v in values.items()) - @wsgi.Controller.authorize - def show(self, req, id): + @wsgi.Controller.authorize("show") + def _show(self, req, id): context = req.environ['manila.context'] params = parse.parse_qs(req.environ.get('QUERY_STRING', '')) user_id = params.get('user_id', [None])[0] @@ -74,12 +78,12 @@ class QuotaSetsController(wsgi.Controller): raise webob.exc.HTTPForbidden() @wsgi.Controller.authorize('show') - def defaults(self, req, id): + def _defaults(self, req, id): context = req.environ['manila.context'] return self._view_builder.detail_list(QUOTAS.get_defaults(context), id) - @wsgi.Controller.authorize - def update(self, req, id, body): + @wsgi.Controller.authorize("update") + def _update(self, req, id, body): context = req.environ['manila.context'] project_id = id bad_keys = [] @@ -166,8 +170,8 @@ class QuotaSetsController(wsgi.Controller): return self._view_builder.detail_list( self._get_quotas(context, id, user_id=user_id)) - @wsgi.Controller.authorize - def delete(self, req, id): + @wsgi.Controller.authorize("delete") + def _delete(self, req, id): context = req.environ['manila.context'] params = parse.parse_qs(req.environ.get('QUERY_STRING', '')) user_id = params.get('user_id', [None])[0] @@ -182,5 +186,57 @@ class QuotaSetsController(wsgi.Controller): raise webob.exc.HTTPForbidden() +class QuotaSetsControllerLegacy(QuotaSetsMixin, wsgi.Controller): + """Deprecated Quota Sets API controller. + + Used by legacy API v1 and v2 microversions from 2.0 to 2.6. + Registered under deprecated API URL 'os-quota-sets'. + """ + + @wsgi.Controller.api_version('1.0', '2.6') + def show(self, req, id): + return self._show(req, id) + + @wsgi.Controller.api_version('1.0', '2.6') + def defaults(self, req, id): + return self._defaults(req, id) + + @wsgi.Controller.api_version('1.0', '2.6') + def update(self, req, id, body): + return self._update(req, id, body) + + @wsgi.Controller.api_version('1.0', '2.6') + def delete(self, req, id): + return self._delete(req, id) + + +class QuotaSetsController(QuotaSetsMixin, wsgi.Controller): + """Quota Sets API controller. + + Used only by API v2 starting from microversion 2.7. + Registered under API URL 'quota-sets'. + """ + + @wsgi.Controller.api_version('2.7') + def show(self, req, id): + return self._show(req, id) + + @wsgi.Controller.api_version('2.7') + def defaults(self, req, id): + return self._defaults(req, id) + + @wsgi.Controller.api_version('2.7') + def update(self, req, id, body): + return self._update(req, id, body) + + @wsgi.Controller.api_version('2.7') + def delete(self, req, id): + return self._delete(req, id) + + +def create_resource_legacy(): + return wsgi.Resource(QuotaSetsControllerLegacy()) + + def create_resource(): return wsgi.Resource(QuotaSetsController()) diff --git a/manila/api/v2/router.py b/manila/api/v2/router.py new file mode 100644 index 0000000000..2e4e5bb2a9 --- /dev/null +++ b/manila/api/v2/router.py @@ -0,0 +1,253 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright (c) 2015 Mirantis inc. +# 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. + +""" +WSGI middleware for OpenStack Share API v2. +""" + +from oslo_log import log + +from manila.api import extensions +import manila.api.openstack +from manila.api.v1 import limits +from manila.api.v1 import scheduler_stats +from manila.api.v1 import security_service +from manila.api.v1 import share_instances +from manila.api.v1 import share_manage +from manila.api.v1 import share_metadata +from manila.api.v1 import share_networks +from manila.api.v1 import share_servers +from manila.api.v1 import share_snapshots +from manila.api.v1 import share_types_extra_specs +from manila.api.v1 import share_unmanage +from manila.api.v2 import availability_zones +from manila.api.v2 import cgsnapshots +from manila.api.v2 import consistency_groups +from manila.api.v2 import quota_class_sets +from manila.api.v2 import quota_sets +from manila.api.v2 import services +from manila.api.v2 import share_types +from manila.api.v2 import shares +from manila.api import versions + +LOG = log.getLogger(__name__) + + +class APIRouter(manila.api.openstack.APIRouter): + """Route API requests. + + Routes requests on the OpenStack API to the appropriate controller + and method. + """ + ExtensionManager = extensions.ExtensionManager + + def _setup_routes(self, mapper, ext_mgr): + self.resources["versions"] = versions.create_resource() + mapper.connect("versions", "/", + controller=self.resources["versions"], + action="index") + + mapper.redirect("", "/") + + self.resources["availability_zones_legacy"] = ( + availability_zones.create_resource_legacy()) + # TODO(vponomaryov): "os-availability-zone" is deprecated + # since v2.7. Remove it when minimum API version becomes equal to + # or greater than v2.7. + mapper.resource("availability-zone", + "os-availability-zone", + controller=self.resources["availability_zones_legacy"]) + + self.resources["availability_zones"] = ( + availability_zones.create_resource()) + mapper.resource("availability-zone", + "availability-zones", + controller=self.resources["availability_zones"]) + + self.resources["services_legacy"] = services.create_resource_legacy() + # TODO(vponomaryov): "os-services" is deprecated + # since v2.7. Remove it when minimum API version becomes equal to + # or greater than v2.7. + mapper.resource("service", + "os-services", + controller=self.resources["services_legacy"]) + + self.resources["services"] = services.create_resource() + mapper.resource("service", + "services", + controller=self.resources["services"]) + + self.resources["quota_sets_legacy"] = ( + quota_sets.create_resource_legacy()) + # TODO(vponomaryov): "os-quota-sets" is deprecated + # since v2.7. Remove it when minimum API version becomes equal to + # or greater than v2.7. + mapper.resource("quota-set", + "os-quota-sets", + controller=self.resources["quota_sets_legacy"], + member={"defaults": "GET"}) + + self.resources["quota_sets"] = quota_sets.create_resource() + mapper.resource("quota-set", + "quota-sets", + controller=self.resources["quota_sets"], + member={"defaults": "GET"}) + + self.resources["quota_class_sets_legacy"] = ( + quota_class_sets.create_resource_legacy()) + # TODO(vponomaryov): "os-quota-class-sets" is deprecated + # since v2.7. Remove it when minimum API version becomes equal to + # or greater than v2.7. + mapper.resource("quota-class-set", + "os-quota-class-sets", + controller=self.resources["quota_class_sets_legacy"]) + + self.resources["quota_class_sets"] = quota_class_sets.create_resource() + mapper.resource("quota-class-set", + "quota-class-sets", + controller=self.resources["quota_class_sets"]) + + self.resources["share_manage"] = share_manage.create_resource() + # TODO(vponomaryov): "os-share-manage" is deprecated + # since v2.7. Remove it when minimum API version becomes equal to + # or greater than v2.7. + mapper.resource("share_manage", + "os-share-manage", + controller=self.resources["share_manage"]) + + self.resources["share_unmanage"] = share_unmanage.create_resource() + # TODO(vponomaryov): "os-share-unmanage" is deprecated + # since v2.7. Remove it when minimum API version becomes equal to + # or greater than v2.7. + mapper.resource("share_unmanage", + "os-share-unmanage", + controller=self.resources["share_unmanage"], + member={"unmanage": "POST"}) + + self.resources["shares"] = shares.create_resource() + mapper.resource("share", "shares", + controller=self.resources["shares"], + collection={"detail": "GET"}, + member={"action": "POST"}) + + mapper.connect("shares", + "/{project_id}/shares/manage", + controller=self.resources["shares"], + action="manage", + conditions={"method": ["POST"]}) + + self.resources["share_instances"] = share_instances.create_resource() + mapper.resource("share_instance", "share_instances", + controller=self.resources["share_instances"], + collection={"detail": "GET"}, + member={"action": "POST"}) + + mapper.connect("share_instance", + "/{project_id}/shares/{share_id}/instances", + controller=self.resources["share_instances"], + action="get_share_instances", + conditions={"method": ["GET"]}) + + self.resources["snapshots"] = share_snapshots.create_resource() + mapper.resource("snapshot", "snapshots", + controller=self.resources["snapshots"], + collection={"detail": "GET"}, + member={"action": "POST"}) + + self.resources["share_metadata"] = share_metadata.create_resource() + share_metadata_controller = self.resources["share_metadata"] + + mapper.resource("share_metadata", "metadata", + controller=share_metadata_controller, + parent_resource=dict(member_name="share", + collection_name="shares")) + + mapper.connect("metadata", + "/{project_id}/shares/{share_id}/metadata", + controller=share_metadata_controller, + action="update_all", + conditions={"method": ["PUT"]}) + + self.resources["limits"] = limits.create_resource() + mapper.resource("limit", "limits", + controller=self.resources["limits"]) + + self.resources["security_services"] = ( + security_service.create_resource()) + mapper.resource("security-service", "security-services", + controller=self.resources["security_services"], + collection={"detail": "GET"}) + + self.resources["share_networks"] = share_networks.create_resource() + mapper.resource(share_networks.RESOURCE_NAME, + "share-networks", + controller=self.resources["share_networks"], + collection={"detail": "GET"}, + member={"action": "POST"}) + + self.resources["share_servers"] = share_servers.create_resource() + mapper.resource("share_server", + "share-servers", + controller=self.resources["share_servers"]) + mapper.connect("details", + "/{project_id}/share-servers/{id}/details", + controller=self.resources["share_servers"], + action="details", + conditions={"method": ["GET"]}) + + self.resources["types"] = share_types.create_resource() + mapper.resource("type", "types", + controller=self.resources["types"], + collection={"detail": "GET", "default": "GET"}, + member={"action": "POST", + "os-share-type-access": "GET", + "share_type_access": "GET"}) + + self.resources["extra_specs"] = ( + share_types_extra_specs.create_resource()) + mapper.resource("extra_spec", "extra_specs", + controller=self.resources["extra_specs"], + parent_resource=dict(member_name="type", + collection_name="types")) + + self.resources["scheduler_stats"] = scheduler_stats.create_resource() + mapper.connect("pools", "/{project_id}/scheduler-stats/pools", + controller=self.resources["scheduler_stats"], + action="pools_index", + conditions={"method": ["GET"]}) + mapper.connect("pools", "/{project_id}/scheduler-stats/pools/detail", + controller=self.resources["scheduler_stats"], + action="pools_detail", + conditions={"method": ["GET"]}) + + self.resources["consistency-groups"] = ( + consistency_groups.create_resource()) + mapper.resource("consistency-group", "consistency-groups", + controller=self.resources["consistency-groups"], + collection={"detail": "GET"}) + mapper.connect("consistency-groups", + "/{project_id}/consistency-groups/{id}/action", + controller=self.resources["consistency-groups"], + action="action", + conditions={"action": ["POST"]}) + + self.resources["cgsnapshots"] = cgsnapshots.create_resource() + mapper.resource("cgsnapshot", "cgsnapshots", + controller=self.resources["cgsnapshots"], + collection={"detail": "GET"}, + member={"members": "GET", "action": "POST"}) diff --git a/manila/api/v1/services.py b/manila/api/v2/services.py similarity index 70% rename from manila/api/v1/services.py rename to manila/api/v2/services.py index 62bebd718a..f9dd73a2ab 100644 --- a/manila/api/v1/services.py +++ b/manila/api/v2/services.py @@ -25,14 +25,18 @@ from manila import utils LOG = log.getLogger(__name__) -class ServiceController(wsgi.Controller): - """The Services API controller for the OpenStack API.""" +class ServiceMixin(object): + """The Services API controller common logic. + + Mixin class that should be inherited by Services API controllers, + which are used for different API URLs and microversions. + """ resource_name = "service" _view_builder_class = services_views.ViewBuilder - @wsgi.Controller.authorize - def index(self, req): + @wsgi.Controller.authorize("index") + def _index(self, req): """Return a list of all running services.""" context = req.environ['manila.context'] @@ -68,8 +72,8 @@ class ServiceController(wsgi.Controller): return self._view_builder.detail_list(services) - @wsgi.Controller.authorize - def update(self, req, id, body): + @wsgi.Controller.authorize("update") + def _update(self, req, id, body): """Enable/Disable scheduling for a service.""" context = req.environ['manila.context'] @@ -93,5 +97,41 @@ class ServiceController(wsgi.Controller): return self._view_builder.summary(data) +class ServiceControllerLegacy(ServiceMixin, wsgi.Controller): + """Deprecated Services API controller. + + Used by legacy API v1 and v2 microversions from 2.0 to 2.6. + Registered under deprecated API URL 'os-services'. + """ + + @wsgi.Controller.api_version('1.0', '2.6') + def index(self, req): + return self._index(req) + + @wsgi.Controller.api_version('1.0', '2.6') + def update(self, req, id, body): + return self._update(req, id, body) + + +class ServiceController(ServiceMixin, wsgi.Controller): + """Services API controller. + + Used only by API v2 starting from microversion 2.7. + Registered under API URL 'services'. + """ + + @wsgi.Controller.api_version('2.7') + def index(self, req): + return self._index(req) + + @wsgi.Controller.api_version('2.7') + def update(self, req, id, body): + return self._update(req, id, body) + + +def create_resource_legacy(): + return wsgi.Resource(ServiceControllerLegacy()) + + def create_resource(): return wsgi.Resource(ServiceController()) diff --git a/manila/api/v1/share_types.py b/manila/api/v2/share_types.py similarity index 97% rename from manila/api/v1/share_types.py rename to manila/api/v2/share_types.py index c9eed82405..52afbade85 100644 --- a/manila/api/v1/share_types.py +++ b/manila/api/v2/share_types.py @@ -37,7 +37,7 @@ class ShareTypesController(wsgi.Controller): def __getattr__(self, key): if key == 'os-share-type-access': - return self._list_project_access + return self.share_type_access return super(self.__class__, self).__getattr__(key) def _notify_share_type_error(self, context, method, payload): @@ -143,7 +143,10 @@ class ShareTypesController(wsgi.Controller): share_type = body['volume_type'] name = share_type.get('name', None) specs = share_type.get('extra_specs', {}) - is_public = share_type.get('os-share-type-access:is_public', True) + is_public = share_type.get( + 'os-share-type-access:is_public', + share_type.get('share_type_access:is_public', True), + ) if name is None or name == "" or len(name) > 255: msg = _("Type name is not valid.") @@ -209,7 +212,7 @@ class ShareTypesController(wsgi.Controller): return webob.Response(status_int=202) @wsgi.Controller.authorize('list_project_access') - def _list_project_access(self, req, id): + def share_type_access(self, req, id): context = req.environ['manila.context'] try: diff --git a/manila/api/v2/shares.py b/manila/api/v2/shares.py new file mode 100644 index 0000000000..76c716cd21 --- /dev/null +++ b/manila/api/v2/shares.py @@ -0,0 +1,150 @@ +# Copyright (c) 2015 Mirantis inc. +# 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. + +from manila.api.openstack import wsgi +from manila.api.v1 import share_manage +from manila.api.v1 import share_unmanage +from manila.api.v1 import shares +from manila.api.views import shares as share_views +from manila import share + + +class ShareController(shares.ShareMixin, + share_manage.ShareManageMixin, + share_unmanage.ShareUnmanageMixin, + wsgi.Controller, + wsgi.AdminActionsMixin): + """The Shares API v2 controller for the OpenStack API.""" + + resource_name = 'share' + _view_builder_class = share_views.ViewBuilder + + def __init__(self): + super(self.__class__, self).__init__() + self.share_api = share.API() + + @wsgi.Controller.api_version("2.4") + def create(self, req, body): + return self._create(req, body) + + @wsgi.Controller.api_version("2.0", "2.3") # noqa + def create(self, req, body): # pylint: disable=E0102 + # Remove consistency group attributes + body.get('share', {}).pop('consistency_group_id', None) + share = self._create(req, body) + return share + + @wsgi.Controller.api_version('2.0', '2.6') + @wsgi.action('os-reset_status') + def share_reset_status_legacy(self, req, id, body): + return self._reset_status(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('reset_status') + def share_reset_status(self, req, id, body): + return self._reset_status(req, id, body) + + @wsgi.Controller.api_version('2.0', '2.6') + @wsgi.action('os-force_delete') + def share_force_delete_legacy(self, req, id, body): + return self._force_delete(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('force_delete') + def share_force_delete(self, req, id, body): + return self._force_delete(req, id, body) + + @wsgi.Controller.api_version('2.5', '2.6', experimental=True) + @wsgi.action("os-migrate_share") + def migrate_share_legacy(self, req, id, body): + return self._migrate_share(req, id, body) + + @wsgi.Controller.api_version('2.7', experimental=True) + @wsgi.action("migrate_share") + def migrate_share(self, req, id, body): + return self._migrate_share(req, id, body) + + @wsgi.Controller.api_version('2.0', '2.6') + @wsgi.action('os-allow_access') + def allow_access_legacy(self, req, id, body): + """Add share access rule.""" + return self._allow_access(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('allow_access') + def allow_access(self, req, id, body): + """Add share access rule.""" + return self._allow_access(req, id, body) + + @wsgi.Controller.api_version('2.0', '2.6') + @wsgi.action('os-deny_access') + def deny_access_legacy(self, req, id, body): + """Remove share access rule.""" + return self._deny_access(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('deny_access') + def deny_access(self, req, id, body): + """Remove share access rule.""" + return self._deny_access(req, id, body) + + @wsgi.Controller.api_version('2.0', '2.6') + @wsgi.action('os-access_list') + def access_list_legacy(self, req, id, body): + """List share access rules.""" + return self._access_list(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('access_list') + def access_list(self, req, id, body): + """List share access rules.""" + return self._access_list(req, id, body) + + @wsgi.Controller.api_version('2.0', '2.6') + @wsgi.action('os-extend') + def extend_legacy(self, req, id, body): + """Extend size of a share.""" + return self._extend(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('extend') + def extend(self, req, id, body): + """Extend size of a share.""" + return self._extend(req, id, body) + + @wsgi.Controller.api_version('2.0', '2.6') + @wsgi.action('os-shrink') + def shrink_legacy(self, req, id, body): + """Shrink size of a share.""" + return self._shrink(req, id, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('shrink') + def shrink(self, req, id, body): + """Shrink size of a share.""" + return self._shrink(req, id, body) + + @wsgi.Controller.api_version('2.7') + def manage(self, req, body): + return self._manage(req, body) + + @wsgi.Controller.api_version('2.7') + @wsgi.action('unmanage') + def unmanage(self, req, id, body=None): + return self._unmanage(req, id, body) + + +def create_resource(): + return wsgi.Resource(ShareController()) diff --git a/manila/api/views/types.py b/manila/api/views/types.py index fa0c98c424..085ff4ba62 100644 --- a/manila/api/views/types.py +++ b/manila/api/views/types.py @@ -21,6 +21,11 @@ class ViewBuilder(common.ViewBuilder): _collection_name = 'types' + _detail_version_modifiers = [ + "add_is_public_attr_core_api_like", + "add_is_public_attr_extension_like", + ] + def show(self, request, share_type, brief=False): """Trim away extraneous share type attributes.""" @@ -38,16 +43,25 @@ class ViewBuilder(common.ViewBuilder): trimmed = { 'id': share_type.get('id'), 'name': share_type.get('name'), - 'os-share-type-access:is_public': share_type.get( - 'is_public', True), 'extra_specs': extra_specs, 'required_extra_specs': required_extra_specs, } + self.update_versioned_resource_dict(request, trimmed, share_type) if brief: return trimmed else: return dict(volume_type=trimmed, share_type=trimmed) + @common.ViewBuilder.versioned_method("2.7") + def add_is_public_attr_core_api_like(self, share_type_dict, share_type): + share_type_dict['share_type_access:is_public'] = share_type.get( + 'is_public', True) + + @common.ViewBuilder.versioned_method("1.0", "2.6") + def add_is_public_attr_extension_like(self, share_type_dict, share_type): + share_type_dict['os-share-type-access:is_public'] = share_type.get( + 'is_public', True) + def index(self, request, share_types): """Index over trimmed share types.""" share_types_list = [self.show(request, share_type, True) diff --git a/manila/tests/api/fakes.py b/manila/tests/api/fakes.py index a7555e16e2..2443f43718 100644 --- a/manila/tests/api/fakes.py +++ b/manila/tests/api/fakes.py @@ -28,7 +28,8 @@ from manila.api.openstack import api_version_request as api_version from manila.api.openstack import wsgi as os_wsgi from manila.api import urlmap from manila.api.v1 import limits -from manila.api.v1 import router +from manila.api.v1 import router as router_v1 +from manila.api.v2 import router as router_v2 from manila.api import versions from manila.common import constants from manila import context @@ -64,7 +65,7 @@ def fake_wsgi(self, req): def wsgi_app(inner_app_v2=None, fake_auth=True, fake_auth_context=None, use_no_auth=False, ext_mgr=None): if not inner_app_v2: - inner_app_v2 = router.APIRouter(ext_mgr) + inner_app_v2 = router_v2.APIRouter(ext_mgr) if fake_auth: if fake_auth_context is not None: @@ -174,28 +175,45 @@ def app(): No auth, just let environ['manila.context'] pass through. """ - api = router.APIRouter() mapper = urlmap.URLMap() - mapper['/v2'] = api - mapper['/v1'] = api + mapper['/v1'] = router_v1.APIRouter() + mapper['/v2'] = router_v2.APIRouter() return mapper fixture_reset_status_with_different_roles = ( { - 'role': 'admin', 'valid_code': 202, + 'role': 'admin', + 'valid_code': 202, 'valid_status': constants.STATUS_ERROR, + 'version': '2.6', }, { - 'role': 'member', 'valid_code': 403, + 'role': 'admin', + 'valid_code': 202, + 'valid_status': constants.STATUS_ERROR, + 'version': '2.7', + }, + { + 'role': 'member', + 'valid_code': 403, 'valid_status': constants.STATUS_AVAILABLE, + 'version': '2.6', + }, + { + 'role': 'member', + 'valid_code': 403, + 'valid_status': constants.STATUS_AVAILABLE, + 'version': '2.7', }, ) fixture_force_delete_with_different_roles = ( - {'role': 'admin', 'resp_code': 202}, - {'role': 'member', 'resp_code': 403}, + {'role': 'admin', 'resp_code': 202, 'version': '2.6'}, + {'role': 'admin', 'resp_code': 202, 'version': '2.7'}, + {'role': 'member', 'resp_code': 403, 'version': '2.6'}, + {'role': 'member', 'resp_code': 403, 'version': '2.7'}, ) diff --git a/manila/tests/api/v1/test_share_instances.py b/manila/tests/api/v1/test_share_instances.py index 116aefb998..8fd4a08fe1 100644 --- a/manila/tests/api/v1/test_share_instances.py +++ b/manila/tests/api/v1/test_share_instances.py @@ -15,7 +15,6 @@ import mock from oslo_config import cfg from oslo_serialization import jsonutils import six -import webob from webob import exc as webob_exc from manila.api.v1 import share_instances @@ -48,12 +47,13 @@ class ShareInstancesAPITest(test.TestCase): def _get_context(self, role): return getattr(self, '%s_context' % role) - def _setup_share_instance_data(self, instance=None): + def _setup_share_instance_data(self, instance=None, version='2.7'): if instance is None: instance = db_utils.create_share(status=constants.STATUS_AVAILABLE, size='1').instance - req = webob.Request.blank( - '/v2/fake/share_instances/%s/action' % instance['id']) + req = fakes.HTTPRequest.blank( + '/v2/fake/share_instances/%s/action' % instance['id'], + version=version) return instance, req def _get_request(self, uri, context=None): @@ -139,11 +139,16 @@ class ShareInstancesAPITest(test.TestCase): webob_exc.HTTPForbidden, target_method, req, *args) def _reset_status(self, ctxt, model, req, db_access_method, - valid_code, valid_status=None, body=None): + valid_code, valid_status=None, body=None, version='2.7'): + if float(version) > 2.6: + action_name = 'reset_status' + else: + action_name = 'os-reset_status' if body is None: - body = {'os-reset_status': {'status': constants.STATUS_ERROR}} + body = {action_name: {'status': constants.STATUS_ERROR}} req.method = 'POST' req.headers['content-type'] = 'application/json' + req.headers['X-Openstack-Manila-Api-Version'] = version req.body = six.b(jsonutils.dumps(body)) req.environ['manila.context'] = ctxt @@ -167,29 +172,40 @@ class ShareInstancesAPITest(test.TestCase): @ddt.unpack def test_share_instances_reset_status_with_different_roles(self, role, valid_code, - valid_status): + valid_status, + version): ctxt = self._get_context(role) - instance, req = self._setup_share_instance_data() - req.headers['X-Openstack-Manila-Api-Version'] = '2.3' + instance, req = self._setup_share_instance_data(version=version) self._reset_status(ctxt, instance, req, db.share_instance_get, - valid_code, valid_status) + valid_code, valid_status, version=version) - @ddt.data(*fakes.fixture_invalid_reset_status_body) - def test_share_instance_invalid_reset_status_body(self, body): + @ddt.data( + ({'os-reset_status': {'x-status': 'bad'}}, '2.6'), + ({'os-reset_status': {'status': 'invalid'}}, '2.6'), + ({'reset_status': {'x-status': 'bad'}}, '2.7'), + ({'reset_status': {'status': 'invalid'}}, '2.7'), + ) + @ddt.unpack + def test_share_instance_invalid_reset_status_body(self, body, version): instance, req = self._setup_share_instance_data() - req.headers['X-Openstack-Manila-Api-Version'] = '2.3' + req.headers['X-Openstack-Manila-Api-Version'] = version self._reset_status(self.admin_context, instance, req, db.share_instance_get, 400, - constants.STATUS_AVAILABLE, body) + constants.STATUS_AVAILABLE, body, version=version) def _force_delete(self, ctxt, model, req, db_access_method, valid_code, - check_model_in_db=False): + check_model_in_db=False, version='2.7'): + if float(version) > 2.6: + action_name = 'force_delete' + else: + action_name = 'os-force_delete' + body = {action_name: {'status': constants.STATUS_ERROR}} req.method = 'POST' req.headers['content-type'] = 'application/json' - req.headers['X-Openstack-Manila-Api-Version'] = '2.3' - req.body = six.b(jsonutils.dumps({'os-force_delete': {}})) + req.headers['X-Openstack-Manila-Api-Version'] = version + req.body = six.b(jsonutils.dumps(body)) req.environ['manila.context'] = ctxt with mock.patch.object( @@ -207,12 +223,13 @@ class ShareInstancesAPITest(test.TestCase): @ddt.data(*fakes.fixture_force_delete_with_different_roles) @ddt.unpack - def test_instance_force_delete_with_different_roles(self, role, resp_code): - instance, req = self._setup_share_instance_data() + def test_instance_force_delete_with_different_roles(self, role, resp_code, + version): + instance, req = self._setup_share_instance_data(version=version) ctxt = self._get_context(role) self._force_delete(ctxt, instance, req, db.share_instance_get, - resp_code) + resp_code, version=version) def test_instance_force_delete_missing(self): instance, req = self._setup_share_instance_data( diff --git a/manila/tests/api/v1/test_share_snapshots.py b/manila/tests/api/v1/test_share_snapshots.py index dc77a24808..5cdd0d0d62 100644 --- a/manila/tests/api/v1/test_share_snapshots.py +++ b/manila/tests/api/v1/test_share_snapshots.py @@ -390,21 +390,26 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase): def _get_context(self, role): return getattr(self, '%s_context' % role) - def _setup_snapshot_data(self, snapshot=None): + def _setup_snapshot_data(self, snapshot=None, version='2.7'): if snapshot is None: share = db_utils.create_share() snapshot = db_utils.create_snapshot( status=constants.STATUS_AVAILABLE, share_id=share['id']) - req = webob.Request.blank('/v2/fake/snapshots/%s/action' % - snapshot['id']) + req = fakes.HTTPRequest.blank('/v2/fake/snapshots/%s/action' % + snapshot['id'], version=version) return snapshot, req def _reset_status(self, ctxt, model, req, db_access_method, - valid_code, valid_status=None, body=None): + valid_code, valid_status=None, body=None, version='2.7'): + if float(version) > 2.6: + action_name = 'reset_status' + else: + action_name = 'os-reset_status' if body is None: - body = {'os-reset_status': {'status': constants.STATUS_ERROR}} + body = {action_name: {'status': constants.STATUS_ERROR}} req.method = 'POST' req.headers['content-type'] = 'application/json' + req.headers['X-Openstack-Manila-Api-Version'] = version req.body = six.b(jsonutils.dumps(body)) req.environ['manila.context'] = ctxt @@ -425,25 +430,37 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase): @ddt.data(*fakes.fixture_reset_status_with_different_roles) @ddt.unpack def test_snapshot_reset_status_with_different_roles(self, role, valid_code, - valid_status): + valid_status, version): ctxt = self._get_context(role) - snapshot, req = self._setup_snapshot_data() + snapshot, req = self._setup_snapshot_data(version=version) self._reset_status(ctxt, snapshot, req, db.share_snapshot_get, - valid_code, valid_status) + valid_code, valid_status, version=version) - @ddt.data(*fakes.fixture_invalid_reset_status_body) - def test_snapshot_invalid_reset_status_body(self, body): - snapshot, req = self._setup_snapshot_data() + @ddt.data( + ({'os-reset_status': {'x-status': 'bad'}}, '2.6'), + ({'reset_status': {'x-status': 'bad'}}, '2.7'), + ({'os-reset_status': {'status': 'invalid'}}, '2.6'), + ({'reset_status': {'status': 'invalid'}}, '2.7'), + ) + @ddt.unpack + def test_snapshot_invalid_reset_status_body(self, body, version): + snapshot, req = self._setup_snapshot_data(version=version) self._reset_status(self.admin_context, snapshot, req, db.share_snapshot_get, 400, - constants.STATUS_AVAILABLE, body) + constants.STATUS_AVAILABLE, body, version=version) - def _force_delete(self, ctxt, model, req, db_access_method, valid_code): + def _force_delete(self, ctxt, model, req, db_access_method, valid_code, + version='2.7'): + if float(version) > 2.6: + action_name = 'force_delete' + else: + action_name = 'os-force_delete' req.method = 'POST' req.headers['content-type'] = 'application/json' - req.body = six.b(jsonutils.dumps({'os-force_delete': {}})) + req.headers['X-Openstack-Manila-Api-Version'] = version + req.body = six.b(jsonutils.dumps({action_name: {}})) req.environ['manila.context'] = ctxt resp = req.get_response(fakes.app()) @@ -453,12 +470,13 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase): @ddt.data(*fakes.fixture_force_delete_with_different_roles) @ddt.unpack - def test_snapshot_force_delete_with_different_roles(self, role, resp_code): + def test_snapshot_force_delete_with_different_roles(self, role, resp_code, + version): ctxt = self._get_context(role) - snapshot, req = self._setup_snapshot_data() + snapshot, req = self._setup_snapshot_data(version=version) self._force_delete(ctxt, snapshot, req, db.share_snapshot_get, - resp_code) + resp_code, version=version) def test_snapshot_force_delete_missing(self): ctxt = self._get_context('admin') diff --git a/manila/tests/api/v1/test_shares.py b/manila/tests/api/v1/test_shares.py index e0f3557bf2..04244014eb 100644 --- a/manila/tests/api/v1/test_shares.py +++ b/manila/tests/api/v1/test_shares.py @@ -41,14 +41,6 @@ from manila import utils CONF = cfg.CONF -def app(): - # no auth, just let environ['manila.context'] pass through - api = fakes.router.APIRouter() - mapper = fakes.urlmap.URLMap() - mapper['/v1'] = api - return mapper - - @ddt.ddt class ShareAPITest(test.TestCase): """Share API Test.""" @@ -230,62 +222,6 @@ class ShareAPITest(test.TestCase): self.assertEqual("fakenetid", create_mock.call_args[1]['share_network_id']) - def test_migrate_share(self): - share = db_utils.create_share() - req = fakes.HTTPRequest.blank('/shares/%s/action' % share['id'], - use_admin_context=True) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('2.5') - req.api_version_request.experimental = True - body = {'os-migrate_share': {'host': 'fake_host'}} - self.mock_object(share_api.API, 'migrate_share') - self.controller.migrate_share(req, share['id'], body) - - def test_migrate_share_no_share_id(self): - req = fakes.HTTPRequest.blank('/shares/%s/action' % 'fake_id', - use_admin_context=True) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('2.5') - req.api_version_request.experimental = True - body = {'os-migrate_share': {'host': 'fake_host'}} - self.mock_object(share_api.API, 'migrate_share') - self.mock_object(share_api.API, 'get', - mock.Mock(side_effect=[exception.NotFound])) - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.migrate_share, - req, 'fake_id', body) - - def test_migrate_share_no_host(self): - share = db_utils.create_share() - req = fakes.HTTPRequest.blank('/shares/%s/action' % share['id'], - use_admin_context=True) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('2.5') - req.api_version_request.experimental = True - body = {'os-migrate_share': {}} - self.mock_object(share_api.API, 'migrate_share') - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.migrate_share, - req, share['id'], body) - - def test_migrate_share_no_host_invalid_force_host_copy(self): - share = db_utils.create_share() - req = fakes.HTTPRequest.blank('/shares/%s/action' % share['id'], - use_admin_context=True) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('2.5') - req.api_version_request.experimental = True - body = {'os-migrate_share': {'host': 'fake_host', - 'force_host_copy': 'fake'}} - self.mock_object(share_api.API, 'migrate_share') - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.migrate_share, - req, share['id'], body) - def test_share_create_from_snapshot_without_share_net_no_parent(self): shr = { "size": 100, @@ -1059,6 +995,27 @@ class ShareAdminActionsAPITest(test.TestCase): actual_model = db_access_method(ctxt, model['id']) self.assertEqual(valid_status, actual_model['status']) + @ddt.data( + { + 'role': 'admin', + 'valid_code': 202, + 'valid_status': constants.STATUS_ERROR, + }, + { + 'role': 'member', + 'valid_code': 403, + 'valid_status': constants.STATUS_AVAILABLE, + }, + ) + @ddt.unpack + def test_share_reset_status_with_different_roles(self, role, valid_code, + valid_status): + share, req = self._setup_share_data() + ctxt = self._get_context(role) + + self._reset_status(ctxt, share, req, db.share_get, valid_code, + valid_status) + @ddt.data(*fakes.fixture_invalid_reset_status_body) def test_share_invalid_reset_status_body(self, body): share, req = self._setup_share_data() @@ -1093,17 +1050,10 @@ class ShareAdminActionsAPITest(test.TestCase): ctxt, model['id']) - @ddt.data(*fakes.fixture_reset_status_with_different_roles) - @ddt.unpack - def test_share_reset_status_with_different_roles(self, role, valid_code, - valid_status): - share, req = self._setup_share_data() - ctxt = self._get_context(role) - - self._reset_status(ctxt, share, req, db.share_get, valid_code, - valid_status) - - @ddt.data(*fakes.fixture_force_delete_with_different_roles) + @ddt.data( + {'role': 'admin', 'resp_code': 202}, + {'role': 'member', 'resp_code': 403}, + ) @ddt.unpack def test_share_force_delete_with_different_roles(self, role, resp_code): share, req = self._setup_share_data() diff --git a/manila/tests/api/v2/__init__.py b/manila/tests/api/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/api/v1/test_availability_zones.py b/manila/tests/api/v2/test_availability_zones.py similarity index 57% rename from manila/tests/api/v1/test_availability_zones.py rename to manila/tests/api/v2/test_availability_zones.py index d1da859c5c..b0721833f3 100644 --- a/manila/tests/api/v1/test_availability_zones.py +++ b/manila/tests/api/v2/test_availability_zones.py @@ -15,8 +15,9 @@ import ddt import mock -from manila.api.v1 import availability_zones +from manila.api.v2 import availability_zones from manila import context +from manila import exception from manila import policy from manila import test from manila.tests.api import fakes @@ -25,16 +26,26 @@ from manila.tests.api import fakes @ddt.ddt class AvailabilityZonesAPITest(test.TestCase): - def test_instantiate_controller(self): - controller = availability_zones.AvailabilityZoneController() + @ddt.data( + availability_zones.AvailabilityZoneControllerLegacy, + availability_zones.AvailabilityZoneController, + ) + def test_instantiate_controller(self, controller): + az_controller = controller() - self.assertTrue(hasattr(controller, "resource_name")) - self.assertEqual("availability_zone", controller.resource_name) - self.assertTrue(hasattr(controller, "_view_builder")) - self.assertTrue(hasattr(controller._view_builder, "detail_list")) + self.assertTrue(hasattr(az_controller, "resource_name")) + self.assertEqual("availability_zone", az_controller.resource_name) + self.assertTrue(hasattr(az_controller, "_view_builder")) + self.assertTrue(hasattr(az_controller._view_builder, "detail_list")) - @ddt.data('1.0', '2.0', '2.6') - def test_index(self, version): + @ddt.data( + ('1.0', availability_zones.AvailabilityZoneControllerLegacy), + ('2.0', availability_zones.AvailabilityZoneControllerLegacy), + ('2.6', availability_zones.AvailabilityZoneControllerLegacy), + ('2.7', availability_zones.AvailabilityZoneController), + ) + @ddt.unpack + def test_index(self, version, controller): azs = [ { "id": "fake_id1", @@ -54,12 +65,12 @@ class AvailabilityZonesAPITest(test.TestCase): mock_policy_check = self.mock_object(policy, 'check_policy') self.mock_object(availability_zones.db, 'availability_zone_get_all', mock.Mock(return_value=azs)) - controller = availability_zones.AvailabilityZoneController() + az_controller = controller() ctxt = context.RequestContext("admin", "fake", True) req = fakes.HTTPRequest.blank('/shares', version=version) req.environ['manila.context'] = ctxt - result = controller.index(req) + result = az_controller.index(req) availability_zones.db.availability_zone_get_all.\ assert_called_once_with(ctxt) @@ -73,3 +84,19 @@ class AvailabilityZonesAPITest(test.TestCase): azs[1].pop("deleted") azs[1].pop("redundant_key") self.assertTrue(azs[1] in result["availability_zones"]) + + @ddt.data( + ('1.0', availability_zones.AvailabilityZoneController), + ('2.0', availability_zones.AvailabilityZoneController), + ('2.6', availability_zones.AvailabilityZoneController), + ('2.7', availability_zones.AvailabilityZoneControllerLegacy), + ) + @ddt.unpack + def test_index_with_unsupported_versions(self, version, controller): + ctxt = context.RequestContext("admin", "fake", True) + req = fakes.HTTPRequest.blank('/shares', version=version) + req.environ['manila.context'] = ctxt + az_controller = controller() + + self.assertRaises( + exception.VersionNotFoundForAPIMethod, az_controller.index, req) diff --git a/manila/tests/api/v1/test_cgsnapshots.py b/manila/tests/api/v2/test_cgsnapshots.py similarity index 96% rename from manila/tests/api/v1/test_cgsnapshots.py rename to manila/tests/api/v2/test_cgsnapshots.py index 339f0d58e8..4c609c98d3 100644 --- a/manila/tests/api/v1/test_cgsnapshots.py +++ b/manila/tests/api/v2/test_cgsnapshots.py @@ -25,7 +25,7 @@ import six import webob from manila.api.openstack import wsgi -import manila.api.v1.cgsnapshots as cgs +import manila.api.v2.cgsnapshots as cgs from manila.common import constants from manila import context from manila import db @@ -518,25 +518,31 @@ class CGSnapshotApiTest(test.TestCase): def _get_context(self, role): return getattr(self, '%s_context' % role) - def _setup_cgsnapshot_data(self, cgsnapshot=None): + def _setup_cgsnapshot_data(self, cgsnapshot=None, version='2.7'): if cgsnapshot is None: cgsnapshot = db_utils.create_cgsnapshot( 'fake_id', status=constants.STATUS_AVAILABLE) - req = webob.Request.blank('/v2/fake/cgsnapshots/%s/action' % - cgsnapshot['id']) - req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '2.4' + req = fakes.HTTPRequest.blank('/v2/fake/cgsnapshots/%s/action' % + cgsnapshot['id'], version=version) + req.headers[wsgi.API_VERSION_REQUEST_HEADER] = version req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True' return cgsnapshot, req @ddt.data(*fakes.fixture_force_delete_with_different_roles) @ddt.unpack def test_cgsnapshot_force_delete_with_different_roles(self, role, - resp_code): + resp_code, version): cgsnap, req = self._setup_cgsnapshot_data() ctxt = self._get_context(role) req.method = 'POST' req.headers['content-type'] = 'application/json' - req.body = six.b(jsonutils.dumps({'os-force_delete': {}})) + if float(version) > 2.6: + action_name = 'force_delete' + else: + action_name = 'os-force_delete' + body = {action_name: {'status': constants.STATUS_ERROR}} + req.body = six.b(jsonutils.dumps(body)) + req.headers['X-Openstack-Manila-Api-Version'] = version req.environ['manila.context'] = ctxt with mock.patch.object( @@ -549,13 +555,18 @@ class CGSnapshotApiTest(test.TestCase): @ddt.data(*fakes.fixture_reset_status_with_different_roles) @ddt.unpack def test_cgsnapshot_reset_status_with_different_roles( - self, role, valid_code, valid_status): + self, role, valid_code, valid_status, version): ctxt = self._get_context(role) - cgsnap, req = self._setup_cgsnapshot_data() - body = {'os-reset_status': {'status': constants.STATUS_ERROR}} + cgsnap, req = self._setup_cgsnapshot_data(version=version) + if float(version) > 2.6: + action_name = 'reset_status' + else: + action_name = 'os-reset_status' + body = {action_name: {'status': constants.STATUS_ERROR}} req.method = 'POST' req.headers['content-type'] = 'application/json' req.body = six.b(jsonutils.dumps(body)) + req.headers['X-Openstack-Manila-Api-Version'] = version req.environ['manila.context'] = ctxt with mock.patch.object( diff --git a/manila/tests/api/v1/test_consistency_groups.py b/manila/tests/api/v2/test_consistency_groups.py similarity index 96% rename from manila/tests/api/v1/test_consistency_groups.py rename to manila/tests/api/v2/test_consistency_groups.py index 8c2ab8080d..ca6137ddc8 100644 --- a/manila/tests/api/v1/test_consistency_groups.py +++ b/manila/tests/api/v2/test_consistency_groups.py @@ -25,7 +25,7 @@ import six import webob from manila.api.openstack import wsgi -import manila.api.v1.consistency_groups as cgs +import manila.api.v2.consistency_groups as cgs from manila.common import constants import manila.consistency_group.api as cg_api from manila import context @@ -64,13 +64,13 @@ class CGApiTest(test.TestCase): def _get_context(self, role): return getattr(self, '%s_context' % role) - def _setup_cg_data(self, cg=None): + def _setup_cg_data(self, cg=None, version='2.7'): if cg is None: cg = db_utils.create_consistency_group( status=constants.STATUS_AVAILABLE) - req = webob.Request.blank('/v2/fake/consistency-groups/%s/action' % - cg['id']) - req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '2.4' + req = fakes.HTTPRequest.blank('/v2/fake/consistency-groups/%s/action' % + cg['id'], version=version) + req.headers[wsgi.API_VERSION_REQUEST_HEADER] = version req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True' return cg, req @@ -629,14 +629,19 @@ class CGApiTest(test.TestCase): @ddt.data(*fakes.fixture_reset_status_with_different_roles) @ddt.unpack def test_consistency_groups_reset_status_with_different_roles( - self, role, valid_code, valid_status): + self, role, valid_code, valid_status, version): ctxt = self._get_context(role) - cg, req = self._setup_cg_data() + cg, req = self._setup_cg_data(version=version) - body = {'os-reset_status': {'status': constants.STATUS_ERROR}} + if float(version) > 2.6: + action_name = 'reset_status' + else: + action_name = 'os-reset_status' + body = {action_name: {'status': constants.STATUS_ERROR}} req.method = 'POST' req.headers['content-type'] = 'application/json' req.body = six.b(jsonutils.dumps(body)) + req.headers['X-Openstack-Manila-Api-Version'] = version req.environ['manila.context'] = ctxt with mock.patch.object( @@ -658,12 +663,18 @@ class CGApiTest(test.TestCase): @ddt.data(*fakes.fixture_force_delete_with_different_roles) @ddt.unpack def test_consistency_group_force_delete_with_different_roles(self, role, - resp_code): + resp_code, + version): ctxt = self._get_context(role) - cg, req = self._setup_cg_data() + cg, req = self._setup_cg_data(version=version) req.method = 'POST' req.headers['content-type'] = 'application/json' - req.body = six.b(jsonutils.dumps({'os-force_delete': {}})) + if float(version) > 2.6: + action_name = 'force_delete' + else: + action_name = 'os-force_delete' + body = {action_name: {}} + req.body = six.b(jsonutils.dumps(body)) req.environ['manila.context'] = ctxt with mock.patch.object( diff --git a/manila/tests/api/v1/test_quota_class_sets.py b/manila/tests/api/v2/test_quota_class_sets.py similarity index 62% rename from manila/tests/api/v1/test_quota_class_sets.py rename to manila/tests/api/v2/test_quota_class_sets.py index 2efbf88511..f0662044e1 100644 --- a/manila/tests/api/v1/test_quota_class_sets.py +++ b/manila/tests/api/v2/test_quota_class_sets.py @@ -26,11 +26,12 @@ from oslo_config import cfg import webob.exc import webob.response -from manila.api.v1 import quota_class_sets +from manila.api.v2 import quota_class_sets from manila import context from manila import exception from manila import policy from manila import test +from manila.tests.api import fakes CONF = cfg.CONF @@ -55,7 +56,16 @@ class QuotaSetsControllerTest(test.TestCase): self.mock_policy_check = self.mock_object( policy, 'check_policy', mock.Mock(return_value=True)) - def test_show_quota(self): + @ddt.data( + ('os-', '1.0', quota_class_sets.QuotaClassSetsControllerLegacy), + ('os-', '2.6', quota_class_sets.QuotaClassSetsControllerLegacy), + ('', '2.7', quota_class_sets.QuotaClassSetsController), + ) + @ddt.unpack + def test_show_quota(self, url, version, controller): + req = fakes.HTTPRequest.blank( + '/fooproject/%squota-class-sets' % url, + version=version, use_admin_context=True) quotas = { "shares": 23, "snapshots": 34, @@ -76,11 +86,11 @@ class QuotaSetsControllerTest(test.TestCase): for k, v in quotas.items(): CONF.set_default('quota_' + k, v) - result = self.controller.show(REQ, self.class_name) + result = controller().show(req, self.class_name) self.assertEqual(expected, result) self.mock_policy_check.assert_called_once_with( - REQ.environ['manila.context'], self.resource_name, 'show') + req.environ['manila.context'], self.resource_name, 'show') def test_show_quota_not_authorized(self): self.mock_object( @@ -95,7 +105,16 @@ class QuotaSetsControllerTest(test.TestCase): self.mock_policy_check.assert_called_once_with( REQ.environ['manila.context'], self.resource_name, 'show') - def test_update_quota(self): + @ddt.data( + ('os-', '1.0', quota_class_sets.QuotaClassSetsControllerLegacy), + ('os-', '2.6', quota_class_sets.QuotaClassSetsControllerLegacy), + ('', '2.7', quota_class_sets.QuotaClassSetsController), + ) + @ddt.unpack + def test_update_quota(self, url, version, controller): + req = fakes.HTTPRequest.blank( + '/fooproject/%squota-class-sets' % url, + version=version, use_admin_context=True) CONF.set_default('quota_shares', 789) body = { 'quota_class_set': { @@ -113,17 +132,17 @@ class QuotaSetsControllerTest(test.TestCase): } } - update_result = self.controller.update( - REQ, self.class_name, body=body) + update_result = controller().update( + req, self.class_name, body=body) self.assertEqual(expected, update_result) - show_result = self.controller.show(REQ, self.class_name) + show_result = controller().show(req, self.class_name) expected['quota_class_set']['id'] = self.class_name self.assertEqual(expected, show_result) self.mock_policy_check.assert_has_calls([mock.call( - REQ.environ['manila.context'], self.resource_name, action_name) + req.environ['manila.context'], self.resource_name, action_name) for action_name in ('update', 'show')]) def test_update_quota_not_authorized(self): @@ -140,3 +159,32 @@ class QuotaSetsControllerTest(test.TestCase): REQ_MEMBER, self.class_name, body=body) self.mock_policy_check.assert_called_once_with( REQ_MEMBER.environ['manila.context'], self.resource_name, 'update') + + @ddt.data( + ('os-', '2.7', quota_class_sets.QuotaClassSetsControllerLegacy), + ('', '2.6', quota_class_sets.QuotaClassSetsController), + ('', '2.0', quota_class_sets.QuotaClassSetsController), + ) + @ddt.unpack + def test_api_not_found(self, url, version, controller): + req = fakes.HTTPRequest.blank( + '/fooproject/%squota-class-sets' % url, version=version) + for method_name in ('show', 'update'): + self.assertRaises( + exception.VersionNotFoundForAPIMethod, + getattr(controller(), method_name), + req, self.class_name) + + @ddt.data( + ('os-', '2.7', quota_class_sets.QuotaClassSetsControllerLegacy), + ('', '2.6', quota_class_sets.QuotaClassSetsController), + ('', '2.0', quota_class_sets.QuotaClassSetsController), + ) + @ddt.unpack + def test_update_api_not_found(self, url, version, controller): + req = fakes.HTTPRequest.blank( + '/fooproject/%squota-class-sets' % url, version=version) + self.assertRaises( + exception.VersionNotFoundForAPIMethod, + controller().update, + req, self.class_name) diff --git a/manila/tests/api/v1/test_quota_sets.py b/manila/tests/api/v2/test_quota_sets.py similarity index 73% rename from manila/tests/api/v1/test_quota_sets.py rename to manila/tests/api/v2/test_quota_sets.py index 23ae74f202..1f5ddd96f7 100644 --- a/manila/tests/api/v1/test_quota_sets.py +++ b/manila/tests/api/v2/test_quota_sets.py @@ -26,11 +26,12 @@ from oslo_config import cfg import webob.exc import webob.response -from manila.api.v1 import quota_sets +from manila.api.v2 import quota_sets from manila import context from manila import exception from manila import policy from manila import test +from manila.tests.api import fakes from manila import utils CONF = cfg.CONF @@ -91,6 +92,35 @@ class QuotaSetsControllerTest(test.TestCase): self.mock_policy_check.assert_called_once_with( REQ.environ['manila.context'], self.resource_name, 'show') + @ddt.data( + ('os-', '1.0', quota_sets.QuotaSetsControllerLegacy, 'defaults'), + ('os-', '2.6', quota_sets.QuotaSetsControllerLegacy, 'defaults'), + ('', '2.7', quota_sets.QuotaSetsController, 'defaults'), + ('os-', '1.0', quota_sets.QuotaSetsControllerLegacy, 'show'), + ('os-', '2.6', quota_sets.QuotaSetsControllerLegacy, 'show'), + ('', '2.7', quota_sets.QuotaSetsController, 'show'), + ) + @ddt.unpack + def test_get_quotas_with_different_api_versions(self, url, version, + controller, method_name): + expected = { + 'quota_set': { + 'id': self.project_id, + 'shares': 50, + 'gigabytes': 1000, + 'snapshots': 50, + 'snapshot_gigabytes': 1000, + 'share_networks': 10, + } + } + req = fakes.HTTPRequest.blank( + '/fooproject/%squota-sets' % url, + version=version, use_admin_context=True) + + result = getattr(controller(), method_name)(req, self.project_id) + + self.assertEqual(expected, result) + @ddt.data(REQ, REQ_WITH_USER) def test_show_quota(self, request): quotas = { @@ -216,7 +246,15 @@ class QuotaSetsControllerTest(test.TestCase): self.mock_policy_check.assert_called_once_with( REQ_MEMBER.environ['manila.context'], self.resource_name, 'update') - def test_update_all_quotas_with_force(self): + @ddt.data( + ('os-quota-sets', '1.0', quota_sets.QuotaSetsControllerLegacy), + ('os-quota-sets', '2.6', quota_sets.QuotaSetsControllerLegacy), + ('quota-sets', '2.7', quota_sets.QuotaSetsController), + ) + @ddt.unpack + def test_update_all_quotas_with_force(self, url, version, controller): + req = fakes.HTTPRequest.blank( + '/fooproject/%s' % url, version=version, use_admin_context=True) quotas = ( ('quota_shares', 13), ('quota_gigabytes', 14), @@ -237,30 +275,37 @@ class QuotaSetsControllerTest(test.TestCase): 'force': True, } } - mock_policy_update_check_call = mock.call( - REQ.environ['manila.context'], self.resource_name, 'update') - mock_policy_show_check_call = mock.call( - REQ.environ['manila.context'], self.resource_name, 'show') - update_result = self.controller.update( - REQ, self.project_id, body=expected) + update_result = controller().update( + req, self.project_id, body=expected) expected['quota_set'].pop('force') expected['quota_set'].pop('tenant_id') self.assertEqual(expected, update_result) - show_result = self.controller.show(REQ, self.project_id) + show_result = controller().show(req, self.project_id) expected['quota_set']['id'] = self.project_id self.assertEqual(expected, show_result) self.mock_policy_check.assert_has_calls([ - mock_policy_update_check_call, mock_policy_show_check_call]) + mock.call(req.environ['manila.context'], + self.resource_name, action) + for action in ('update', 'show') + ]) - def test_delete_tenant_quota(self): + @ddt.data( + ('os-quota-sets', '1.0', quota_sets.QuotaSetsControllerLegacy), + ('os-quota-sets', '2.6', quota_sets.QuotaSetsControllerLegacy), + ('quota-sets', '2.7', quota_sets.QuotaSetsController), + ) + @ddt.unpack + def test_delete_tenant_quota(self, url, version, controller): self.mock_object(quota_sets.QUOTAS, 'destroy_all_by_project_and_user') self.mock_object(quota_sets.QUOTAS, 'destroy_all_by_project') + req = fakes.HTTPRequest.blank( + '/fooproject/%s' % url, version=version, use_admin_context=True) - result = self.controller.delete(REQ, self.project_id) + result = controller().delete(req, self.project_id) self.assertTrue( utils.IsAMatcher(webob.response.Response) == result @@ -270,9 +315,9 @@ class QuotaSetsControllerTest(test.TestCase): self.assertFalse( quota_sets.QUOTAS.destroy_all_by_project_and_user.called) quota_sets.QUOTAS.destroy_all_by_project.assert_called_once_with( - REQ.environ['manila.context'], self.project_id) + req.environ['manila.context'], self.project_id) self.mock_policy_check.assert_called_once_with( - REQ.environ['manila.context'], self.resource_name, 'delete') + req.environ['manila.context'], self.resource_name, 'delete') def test_delete_user_quota(self): project_id = 'foo_project_id' @@ -303,3 +348,30 @@ class QuotaSetsControllerTest(test.TestCase): REQ_MEMBER, self.project_id) self.mock_policy_check.assert_called_once_with( REQ_MEMBER.environ['manila.context'], self.resource_name, 'delete') + + @ddt.data( + ('os-quota-sets', '2.7', quota_sets.QuotaSetsControllerLegacy), + ('quota-sets', '2.6', quota_sets.QuotaSetsController), + ('quota-sets', '2.0', quota_sets.QuotaSetsController), + ) + @ddt.unpack + def test_api_not_found(self, url, version, controller): + req = fakes.HTTPRequest.blank('/fooproject/%s' % url, version=version) + for method_name in ('show', 'defaults', 'delete'): + self.assertRaises( + exception.VersionNotFoundForAPIMethod, + getattr(controller(), method_name), + req, self.project_id) + + @ddt.data( + ('os-quota-sets', '2.7', quota_sets.QuotaSetsControllerLegacy), + ('quota-sets', '2.6', quota_sets.QuotaSetsController), + ('quota-sets', '2.0', quota_sets.QuotaSetsController), + ) + @ddt.unpack + def test_update_api_not_found(self, url, version, controller): + req = fakes.HTTPRequest.blank('/fooproject/%s' % url, version=version) + self.assertRaises( + exception.VersionNotFoundForAPIMethod, + controller().update, + req, self.project_id) diff --git a/manila/tests/api/v1/test_services.py b/manila/tests/api/v2/test_services.py similarity index 68% rename from manila/tests/api/v1/test_services.py rename to manila/tests/api/v2/test_services.py index a99c80e8cc..941eb9286a 100644 --- a/manila/tests/api/v1/test_services.py +++ b/manila/tests/api/v2/test_services.py @@ -17,10 +17,11 @@ import datetime +import ddt import mock from oslo_utils import timeutils -from manila.api.v1 import services +from manila.api.v2 import services from manila import context from manila import db from manila import exception @@ -106,41 +107,6 @@ fake_response_service_list = {'services': [ ]} -class FakeRequest(object): - environ = {"manila.context": context.get_admin_context()} - GET = {} - - -class FakeRequestWithBinary(object): - environ = {"manila.context": context.get_admin_context()} - GET = {"binary": "manila-share"} - - -class FakeRequestWithHost(object): - environ = {"manila.context": context.get_admin_context()} - GET = {"host": "host1"} - - -class FakeRequestWithZone(object): - environ = {"manila.context": context.get_admin_context()} - GET = {"zone": "manila1"} - - -class FakeRequestWithStatus(object): - environ = {"manila.context": context.get_admin_context()} - GET = {"status": "enabled"} - - -class FakeRequestWithState(object): - environ = {"manila.context": context.get_admin_context()} - GET = {"state": "up"} - - -class FakeRequestWithHostBinary(object): - environ = {"manila.context": context.get_admin_context()} - GET = {"host": "host1", "binary": "manila-share"} - - def fake_service_get_all(context): return fake_services_list @@ -172,10 +138,11 @@ def fake_utcnow(): return datetime.datetime(2012, 10, 29, 13, 42, 11) +@ddt.ddt class ServicesTest(test.TestCase): def setUp(self): - super(ServicesTest, self).setUp() + super(self.__class__, self).setUp() self.mock_object(db, "service_get_all", fake_service_get_all) self.mock_object(timeutils, "utcnow", fake_utcnow) @@ -184,22 +151,31 @@ class ServicesTest(test.TestCase): self.mock_object(db, "service_update", fake_service_update) self.context = context.get_admin_context() self.controller = services.ServiceController() + self.controller_legacy = services.ServiceControllerLegacy() self.resource_name = self.controller.resource_name self.mock_policy_check = self.mock_object( policy, 'check_policy', mock.Mock(return_value=True)) - def tearDown(self): - super(ServicesTest, self).tearDown() + @ddt.data( + ('os-services', '1.0', services.ServiceControllerLegacy), + ('os-services', '2.6', services.ServiceControllerLegacy), + ('services', '2.7', services.ServiceController), + ) + @ddt.unpack + def test_services_list(self, url, version, controller): + req = fakes.HTTPRequest.blank('/%s' % url, version=version) + req.environ['manila.context'] = self.context + + res_dict = controller().index(req) - def test_services_list(self): - req = FakeRequest() - res_dict = self.controller.index(req) self.assertEqual(fake_response_service_list, res_dict) self.mock_policy_check.assert_called_once_with( req.environ['manila.context'], self.resource_name, 'index') def test_services_list_with_host(self): - req = FakeRequestWithHost() + req = fakes.HTTPRequest.blank('/services?host=host1', version='2.7') + req.environ['manila.context'] = self.context + res_dict = self.controller.index(req) response = {'services': [ @@ -211,8 +187,12 @@ class ServicesTest(test.TestCase): req.environ['manila.context'], self.resource_name, 'index') def test_services_list_with_binary(self): - req = FakeRequestWithBinary() + req = fakes.HTTPRequest.blank( + '/services?binary=manila-share', version='2.7') + req.environ['manila.context'] = self.context + res_dict = self.controller.index(req) + response = {'services': [ fake_response_service_list['services'][1], fake_response_service_list['services'][3], @@ -223,8 +203,11 @@ class ServicesTest(test.TestCase): req.environ['manila.context'], self.resource_name, 'index') def test_services_list_with_zone(self): - req = FakeRequestWithZone() + req = fakes.HTTPRequest.blank('/services?zone=manila1', version='2.7') + req.environ['manila.context'] = self.context + res_dict = self.controller.index(req) + response = {'services': [ fake_response_service_list['services'][0], fake_response_service_list['services'][1], @@ -234,8 +217,12 @@ class ServicesTest(test.TestCase): req.environ['manila.context'], self.resource_name, 'index') def test_services_list_with_status(self): - req = FakeRequestWithStatus() + req = fakes.HTTPRequest.blank( + '/services?status=enabled', version='2.7') + req.environ['manila.context'] = self.context + res_dict = self.controller.index(req) + response = {'services': [ fake_response_service_list['services'][2], ]} @@ -244,8 +231,11 @@ class ServicesTest(test.TestCase): req.environ['manila.context'], self.resource_name, 'index') def test_services_list_with_state(self): - req = FakeRequestWithState() + req = fakes.HTTPRequest.blank('/services?state=up', version='2.7') + req.environ['manila.context'] = self.context + res_dict = self.controller.index(req) + response = {'services': [ fake_response_service_list['services'][0], fake_response_service_list['services'][1], @@ -255,25 +245,77 @@ class ServicesTest(test.TestCase): req.environ['manila.context'], self.resource_name, 'index') def test_services_list_with_host_binary(self): - req = FakeRequestWithHostBinary() + req = fakes.HTTPRequest.blank( + "/services?binary=manila-share&state=up", version='2.7') + req.environ['manila.context'] = self.context + res_dict = self.controller.index(req) + response = {'services': [fake_response_service_list['services'][1], ]} self.assertEqual(response, res_dict) self.mock_policy_check.assert_called_once_with( req.environ['manila.context'], self.resource_name, 'index') - def test_services_enable(self): + @ddt.data( + ('os-services', '1.0', services.ServiceControllerLegacy), + ('os-services', '2.6', services.ServiceControllerLegacy), + ('services', '2.7', services.ServiceController), + ) + @ddt.unpack + def test_services_enable(self, url, version, controller): body = {'host': 'host1', 'binary': 'manila-share'} - req = fakes.HTTPRequest.blank('/v1/fake/os-services/enable') - res_dict = self.controller.update(req, "enable", body) + req = fakes.HTTPRequest.blank('/fooproject/%s' % url, version=version) + + res_dict = controller().update(req, "enable", body) + self.assertFalse(res_dict['disabled']) self.mock_policy_check.assert_called_once_with( req.environ['manila.context'], self.resource_name, 'update') - def test_services_disable(self): - req = fakes.HTTPRequest.blank('/v1/fake/os-services/disable') + @ddt.data( + ('os-services', '1.0', services.ServiceControllerLegacy), + ('os-services', '2.6', services.ServiceControllerLegacy), + ('services', '2.7', services.ServiceController), + ) + @ddt.unpack + def test_services_disable(self, url, version, controller): + req = fakes.HTTPRequest.blank( + '/fooproject/%s/disable' % url, version=version) body = {'host': 'host1', 'binary': 'manila-share'} - res_dict = self.controller.update(req, "disable", body) + + res_dict = controller().update(req, "disable", body) + self.assertTrue(res_dict['disabled']) self.mock_policy_check.assert_called_once_with( req.environ['manila.context'], self.resource_name, 'update') + + @ddt.data( + ('os-services', '2.7', services.ServiceControllerLegacy), + ('services', '2.6', services.ServiceController), + ('services', '1.0', services.ServiceController), + ) + @ddt.unpack + def test_services_update_legacy_url_2_dot_7_api_not_found(self, url, + version, + controller): + req = fakes.HTTPRequest.blank( + '/fooproject/%s/fake' % url, version=version) + body = {'host': 'host1', 'binary': 'manila-share'} + + self.assertRaises( + exception.VersionNotFoundForAPIMethod, + controller().update, + req, "disable", body, + ) + + @ddt.data( + ('os-services', '2.7', services.ServiceControllerLegacy), + ('services', '2.6', services.ServiceController), + ('services', '1.0', services.ServiceController), + ) + @ddt.unpack + def test_services_list_api_not_found(self, url, version, controller): + req = fakes.HTTPRequest.blank('/fooproject/%s' % url, version=version) + + self.assertRaises( + exception.VersionNotFoundForAPIMethod, controller().index, req) diff --git a/manila/tests/api/v1/test_share_types.py b/manila/tests/api/v2/test_share_types.py similarity index 97% rename from manila/tests/api/v1/test_share_types.py rename to manila/tests/api/v2/test_share_types.py index 13732f87fc..f61b109534 100644 --- a/manila/tests/api/v1/test_share_types.py +++ b/manila/tests/api/v2/test_share_types.py @@ -21,7 +21,7 @@ from oslo_utils import timeutils import six import webob -from manila.api.v1 import share_types as types +from manila.api.v2 import share_types as types from manila.api.views import types as views_types from manila.common import constants from manila import context @@ -217,7 +217,14 @@ class ShareTypesAPITest(test.TestCase): policy.check_policy.assert_called_once_with( req.environ['manila.context'], self.resource_name, 'default') - def test_view_builder_show(self): + @ddt.data( + ('1.0', 'os-share-type-access'), + ('2.0', 'os-share-type-access'), + ('2.6', 'os-share-type-access'), + ('2.7', 'share_type_access'), + ) + @ddt.unpack + def test_view_builder_show(self, version, prefix): view_builder = views_types.ViewBuilder() now = timeutils.isotime() @@ -232,14 +239,16 @@ class ShareTypesAPITest(test.TestCase): id=42, ) - request = fakes.HTTPRequest.blank("/v2") + request = fakes.HTTPRequest.blank("/v%s" % version[0], version=version) + request.headers['X-Openstack-Manila-Api-Version'] = version + output = view_builder.show(request, raw_share_type) self.assertIn('share_type', output) expected_share_type = { 'name': 'new_type', 'extra_specs': {}, - 'os-share-type-access:is_public': True, + '%s:is_public' % prefix: True, 'required_extra_specs': {}, 'id': 42, } @@ -444,7 +453,7 @@ class ShareTypeAccessTest(test.TestCase): use_admin_context=True) self.assertRaises(webob.exc.HTTPNotFound, - self.controller._list_project_access, + self.controller.share_type_access, req, '1') def test_list_type_access_private(self): @@ -453,7 +462,7 @@ class ShareTypeAccessTest(test.TestCase): {'share_type_id': '2', 'project_id': PROJ3_UUID}, ]} - result = self.controller._list_project_access(self.req, '2') + result = self.controller.share_type_access(self.req, '2') self.assertEqual(expected, result) @@ -464,7 +473,7 @@ class ShareTypeAccessTest(test.TestCase): raise exception.PolicyNotAuthorized(action='index') self.assertRaises(webob.exc.HTTPForbidden, - self.controller._list_project_access, + self.controller.share_type_access, req, 'fake') def test_list_type_with_admin_default_proj1(self): diff --git a/manila/tests/api/v2/test_shares.py b/manila/tests/api/v2/test_shares.py new file mode 100644 index 0000000000..c940107c8b --- /dev/null +++ b/manila/tests/api/v2/test_shares.py @@ -0,0 +1,1446 @@ +# Copyright (c) 2015 Mirantis inc. +# 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 ddt +import mock +from oslo_config import cfg +from oslo_serialization import jsonutils +import six +import webob + +from manila.api import common +from manila.api.openstack import api_version_request as api_version +from manila.api.v2 import shares +from manila.common import constants +from manila import context +from manila import db +from manila import exception +from manila.share import api as share_api +from manila.share import share_types +from manila import test +from manila.tests.api.contrib import stubs +from manila.tests.api import fakes +from manila.tests import db_utils +from manila import utils + +CONF = cfg.CONF + + +@ddt.ddt +class ShareAPITest(test.TestCase): + """Share API Test.""" + + def setUp(self): + super(self.__class__, self).setUp() + self.controller = shares.ShareController() + self.mock_object(db, 'availability_zone_get') + self.mock_object(share_api.API, 'get_all', + stubs.stub_get_all_shares) + self.mock_object(share_api.API, 'get', + stubs.stub_share_get) + self.mock_object(share_api.API, 'update', stubs.stub_share_update) + self.mock_object(share_api.API, 'delete', stubs.stub_share_delete) + self.mock_object(share_api.API, 'get_snapshot', + stubs.stub_snapshot_get) + self.maxDiff = None + self.share = { + "size": 100, + "display_name": "Share Test Name", + "display_description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1", + "is_public": False, + } + self.create_mock = mock.Mock( + return_value=stubs.stub_share( + '1', + display_name=self.share['display_name'], + display_description=self.share['display_description'], + size=100, + share_proto=self.share['share_proto'].upper(), + availability_zone=self.share['availability_zone']) + ) + self.vt = { + 'id': 'fake_volume_type_id', + 'name': 'fake_volume_type_name', + } + CONF.set_default("default_share_type", None) + + def _get_expected_share_detailed_response(self, values=None, admin=False): + share = { + 'id': '1', + 'name': 'displayname', + 'availability_zone': 'fakeaz', + 'description': 'displaydesc', + 'export_location': 'fake_location', + 'export_locations': ['fake_location', 'fake_location2'], + 'project_id': 'fakeproject', + 'host': 'fakehost', + 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'share_proto': 'FAKEPROTO', + 'metadata': {}, + 'size': 1, + 'snapshot_id': '2', + 'share_network_id': None, + 'status': 'fakestatus', + 'share_type': '1', + 'volume_type': '1', + 'snapshot_support': True, + 'is_public': False, + 'consistency_group_id': None, + 'source_cgsnapshot_member_id': None, + 'task_state': None, + 'share_type_name': None, + 'links': [ + { + 'href': 'http://localhost/v1/fake/shares/1', + 'rel': 'self' + }, + { + 'href': 'http://localhost/fake/shares/1', + 'rel': 'bookmark' + } + ], + } + if values: + if 'display_name' in values: + values['name'] = values.pop('display_name') + if 'display_description' in values: + values['description'] = values.pop('display_description') + share.update(values) + if share.get('share_proto'): + share['share_proto'] = share['share_proto'].upper() + if admin: + share['share_server_id'] = 'fake_share_server_id' + return {'share': share} + + @ddt.data("2.0", "2.1") + def test_share_create_original(self, microversion): + self.mock_object(share_api.API, 'create', self.create_mock) + body = {"share": copy.deepcopy(self.share)} + req = fakes.HTTPRequest.blank('/shares', version=microversion) + + res_dict = self.controller.create(req, body) + + expected = self._get_expected_share_detailed_response(self.share) + expected['share'].pop('snapshot_support') + expected['share'].pop('share_type_name') + expected['share'].pop('task_state') + expected['share'].pop('consistency_group_id') + expected['share'].pop('source_cgsnapshot_member_id') + self.assertEqual(expected, res_dict) + + @ddt.data("2.2", "2.3") + def test_share_create_with_snapshot_support_without_cg(self, microversion): + self.mock_object(share_api.API, 'create', self.create_mock) + body = {"share": copy.deepcopy(self.share)} + req = fakes.HTTPRequest.blank('/shares', version=microversion) + + res_dict = self.controller.create(req, body) + + expected = self._get_expected_share_detailed_response(self.share) + expected['share'].pop('share_type_name') + expected['share'].pop('task_state') + expected['share'].pop('consistency_group_id') + expected['share'].pop('source_cgsnapshot_member_id') + self.assertEqual(expected, res_dict) + + @ddt.data("2.4", "2.5") + def test_share_create_with_consistency_group(self, microversion): + self.mock_object(share_api.API, 'create', self.create_mock) + body = {"share": copy.deepcopy(self.share)} + req = fakes.HTTPRequest.blank('/shares', version=microversion) + + res_dict = self.controller.create(req, body) + + expected = self._get_expected_share_detailed_response(self.share) + expected['share'].pop('share_type_name') + if (api_version.APIVersionRequest(microversion) == + api_version.APIVersionRequest('2.4')): + expected['share'].pop('task_state') + 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)) + CONF.set_default("default_share_type", self.vt['name']) + self.mock_object(share_api.API, 'create', self.create_mock) + + body = {"share": copy.deepcopy(self.share)} + req = fakes.HTTPRequest.blank('/shares', version='2.7') + res_dict = self.controller.create(req, body) + + expected = self._get_expected_share_detailed_response(self.share) + share_types.get_share_type_by_name.assert_called_once_with( + utils.IsAMatcher(context.RequestContext), self.vt['name']) + self.assertEqual(expected, res_dict) + + def test_share_create_with_invalid_default_share_type(self): + self.mock_object( + share_types, 'get_default_share_type', + mock.Mock(side_effect=exception.ShareTypeNotFoundByName( + self.vt['name'])), + ) + CONF.set_default("default_share_type", self.vt['name']) + req = fakes.HTTPRequest.blank('/shares', version='2.7') + self.assertRaises(exception.ShareTypeNotFoundByName, + self.controller.create, req, {'share': self.share}) + share_types.get_default_share_type.assert_called_once_with() + + def test_share_create_with_share_net(self): + shr = { + "size": 100, + "name": "Share Test Name", + "description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1", + "share_network_id": "fakenetid" + } + create_mock = mock.Mock(return_value=stubs.stub_share('1', + display_name=shr['name'], + display_description=shr['description'], + size=shr['size'], + share_proto=shr['share_proto'].upper(), + availability_zone=shr['availability_zone'], + share_network_id=shr['share_network_id'])) + self.mock_object(share_api.API, 'create', create_mock) + self.mock_object(share_api.API, 'get_share_network', mock.Mock( + return_value={'id': 'fakenetid'})) + + body = {"share": copy.deepcopy(shr)} + req = fakes.HTTPRequest.blank('/shares', version='2.7') + res_dict = self.controller.create(req, body) + + expected = self._get_expected_share_detailed_response(shr) + self.assertEqual(expected, res_dict) + self.assertEqual("fakenetid", + create_mock.call_args[1]['share_network_id']) + + @ddt.data('2.5', '2.6', '2.7') + def test_migrate_share(self, version): + share = db_utils.create_share() + req = fakes.HTTPRequest.blank('/shares/%s/action' % share['id'], + use_admin_context=True) + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.api_version_request = api_version.APIVersionRequest(version) + req.api_version_request.experimental = True + if float(version) > 2.6: + body = {'migrate_share': {'host': 'fake_host'}} + method = 'migrate_share' + else: + body = {'os-migrate_share': {'host': 'fake_host'}} + method = 'migrate_share_legacy' + self.mock_object(share_api.API, 'migrate_share') + getattr(self.controller, method)(req, share['id'], body) + + @ddt.data('2.5', '2.6', '2.7') + def test_migrate_share_no_share_id(self, version): + req = fakes.HTTPRequest.blank('/shares/%s/action' % 'fake_id', + use_admin_context=True, version=version) + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.api_version_request.experimental = True + if float(version) > 2.6: + body = {'migrate_share': {}} + method = 'migrate_share' + else: + body = {'os-migrate_share': {}} + method = 'migrate_share_legacy' + self.mock_object(share_api.API, 'migrate_share') + self.mock_object(share_api.API, 'get', + mock.Mock(side_effect=[exception.NotFound])) + self.assertRaises(webob.exc.HTTPNotFound, + getattr(self.controller, method), + req, 'fake_id', body) + + @ddt.data('2.5', '2.6', '2.7') + def test_migrate_share_no_host(self, version): + share = db_utils.create_share() + req = fakes.HTTPRequest.blank('/shares/%s/action' % share['id'], + use_admin_context=True) + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.api_version_request = api_version.APIVersionRequest(version) + req.api_version_request.experimental = True + if float(version) > 2.6: + body = {'migrate_share': {}} + method = 'migrate_share' + else: + body = {'os-migrate_share': {}} + method = 'migrate_share_legacy' + self.mock_object(share_api.API, 'migrate_share') + + self.assertRaises(webob.exc.HTTPBadRequest, + getattr(self.controller, method), + req, share['id'], body) + + def test_migrate_share_no_host_invalid_force_host_copy(self): + share = db_utils.create_share() + req = fakes.HTTPRequest.blank('/shares/%s/action' % share['id'], + use_admin_context=True, version='2.7') + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.api_version_request.experimental = True + body = {'os-migrate_share': {'host': 'fake_host', + 'force_host_copy': 'fake'}} + self.mock_object(share_api.API, 'migrate_share') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.migrate_share, + req, share['id'], body) + + def test_share_create_from_snapshot_without_share_net_no_parent(self): + shr = { + "size": 100, + "name": "Share Test Name", + "description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1", + "snapshot_id": 333, + "share_network_id": None, + } + create_mock = mock.Mock(return_value=stubs.stub_share('1', + display_name=shr['name'], + display_description=shr['description'], + size=shr['size'], + share_proto=shr['share_proto'].upper(), + availability_zone=shr['availability_zone'], + snapshot_id=shr['snapshot_id'], + share_network_id=shr['share_network_id'])) + self.mock_object(share_api.API, 'create', create_mock) + body = {"share": copy.deepcopy(shr)} + req = fakes.HTTPRequest.blank('/shares', version='2.7') + res_dict = self.controller.create(req, body) + expected = self._get_expected_share_detailed_response(shr) + self.assertEqual(expected, res_dict) + + def test_share_create_from_snapshot_without_share_net_parent_exists(self): + shr = { + "size": 100, + "name": "Share Test Name", + "description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1", + "snapshot_id": 333, + "share_network_id": None, + } + parent_share_net = 444 + create_mock = mock.Mock(return_value=stubs.stub_share('1', + display_name=shr['name'], + display_description=shr['description'], + size=shr['size'], + share_proto=shr['share_proto'].upper(), + availability_zone=shr['availability_zone'], + snapshot_id=shr['snapshot_id'], + share_network_id=shr['share_network_id'])) + self.mock_object(share_api.API, 'create', create_mock) + self.mock_object(share_api.API, 'get_snapshot', + stubs.stub_snapshot_get) + self.mock_object(share_api.API, 'get', mock.Mock( + return_value={'share_network_id': parent_share_net})) + self.mock_object(share_api.API, 'get_share_network', mock.Mock( + return_value={'id': parent_share_net})) + + body = {"share": copy.deepcopy(shr)} + req = fakes.HTTPRequest.blank('/shares', version='2.7') + res_dict = self.controller.create(req, body) + expected = self._get_expected_share_detailed_response(shr) + self.assertEqual(expected, res_dict) + self.assertEqual(parent_share_net, + create_mock.call_args[1]['share_network_id']) + + def test_share_create_from_snapshot_with_share_net_equals_parent(self): + parent_share_net = 444 + shr = { + "size": 100, + "name": "Share Test Name", + "description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1", + "snapshot_id": 333, + "share_network_id": parent_share_net + } + create_mock = mock.Mock(return_value=stubs.stub_share('1', + display_name=shr['name'], + display_description=shr['description'], + size=shr['size'], + share_proto=shr['share_proto'].upper(), + availability_zone=shr['availability_zone'], + snapshot_id=shr['snapshot_id'], + share_network_id=shr['share_network_id'])) + self.mock_object(share_api.API, 'create', create_mock) + self.mock_object(share_api.API, 'get_snapshot', + stubs.stub_snapshot_get) + self.mock_object(share_api.API, 'get', mock.Mock( + return_value={'share_network_id': parent_share_net})) + self.mock_object(share_api.API, 'get_share_network', mock.Mock( + return_value={'id': parent_share_net})) + + body = {"share": copy.deepcopy(shr)} + req = fakes.HTTPRequest.blank('/shares', version='2.7') + res_dict = self.controller.create(req, body) + expected = self._get_expected_share_detailed_response(shr) + self.assertEqual(expected, res_dict) + self.assertEqual(parent_share_net, + create_mock.call_args[1]['share_network_id']) + + def test_share_create_from_snapshot_invalid_share_net(self): + self.mock_object(share_api.API, 'create') + shr = { + "size": 100, + "name": "Share Test Name", + "description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1", + "snapshot_id": 333, + "share_network_id": 1234 + } + body = {"share": shr} + req = fakes.HTTPRequest.blank('/shares', version='2.7') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_share_creation_fails_with_bad_size(self): + shr = {"size": '', + "name": "Share Test Name", + "description": "Share Test Desc", + "share_proto": "fakeproto", + "availability_zone": "zone1:host1"} + body = {"share": shr} + req = fakes.HTTPRequest.blank('/shares', version='2.7') + self.assertRaises(exception.InvalidInput, + self.controller.create, req, body) + + def test_share_create_no_body(self): + req = fakes.HTTPRequest.blank('/shares', version='2.7') + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.create, req, {}) + + def test_share_create_invalid_availability_zone(self): + self.mock_object( + db, + 'availability_zone_get', + mock.Mock(side_effect=exception.AvailabilityZoneNotFound(id='id')) + ) + body = {"share": copy.deepcopy(self.share)} + + req = fakes.HTTPRequest.blank('/shares', version='2.7') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, + req, + body) + + def test_share_show(self): + req = fakes.HTTPRequest.blank('/shares/1') + expected = self._get_expected_share_detailed_response() + expected['share'].pop('snapshot_support') + expected['share'].pop('share_type_name') + expected['share'].pop('task_state') + expected['share'].pop('consistency_group_id') + expected['share'].pop('source_cgsnapshot_member_id') + + res_dict = self.controller.show(req, '1') + + self.assertEqual(expected, res_dict) + + def test_share_show_with_consistency_group(self): + req = fakes.HTTPRequest.blank('/shares/1', version='2.4') + expected = self._get_expected_share_detailed_response() + expected['share'].pop('share_type_name') + expected['share'].pop('task_state') + + res_dict = self.controller.show(req, '1') + + self.assertEqual(expected, res_dict) + + def test_share_show_with_share_type_name(self): + req = fakes.HTTPRequest.blank('/shares/1', version='2.6') + 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 + expected['share']['share_type_name'] = None + expected['share']['task_state'] = None + self.assertEqual(expected, res_dict) + + def test_share_show_admin(self): + req = fakes.HTTPRequest.blank('/shares/1', use_admin_context=True) + expected = self._get_expected_share_detailed_response(admin=True) + expected['share'].pop('snapshot_support') + expected['share'].pop('share_type_name') + expected['share'].pop('task_state') + expected['share'].pop('consistency_group_id') + expected['share'].pop('source_cgsnapshot_member_id') + + res_dict = self.controller.show(req, '1') + + self.assertEqual(expected, res_dict) + + def test_share_show_no_share(self): + self.mock_object(share_api.API, 'get', + stubs.stub_share_get_notfound) + req = fakes.HTTPRequest.blank('/shares/1') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, + req, '1') + + def test_share_delete(self): + req = fakes.HTTPRequest.blank('/shares/1') + resp = self.controller.delete(req, 1) + self.assertEqual(202, resp.status_int) + + 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(202, resp.status_int) + + 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} + + req = fakes.HTTPRequest.blank('/share/1') + res_dict = self.controller.update(req, 1, body) + self.assertEqual(shr["display_name"], res_dict['share']["name"]) + self.assertEqual(shr["display_description"], + res_dict['share']["description"]) + 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="2.4") + 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}) + self.assertNotEqual(res_dict['share']["size"], self.share["size"]) + + def test_share_delete_no_share(self): + self.mock_object(share_api.API, 'get', + stubs.stub_share_get_notfound) + req = fakes.HTTPRequest.blank('/shares/1') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, + req, + 1) + + def _share_list_summary_with_search_opts(self, use_admin_context): + search_opts = { + 'name': 'fake_name', + 'status': constants.STATUS_AVAILABLE, + 'share_server_id': 'fake_share_server_id', + 'share_type_id': 'fake_share_type_id', + 'snapshot_id': 'fake_snapshot_id', + 'host': 'fake_host', + 'share_network_id': 'fake_share_network_id', + 'metadata': '%7B%27k1%27%3A+%27v1%27%7D', # serialized k1=v1 + 'extra_specs': '%7B%27k2%27%3A+%27v2%27%7D', # serialized k2=v2 + 'sort_key': 'fake_sort_key', + 'sort_dir': 'fake_sort_dir', + 'limit': '1', + 'offset': '1', + 'is_public': 'False', + } + # fake_key should be filtered for non-admin + url = '/shares?fake_key=fake_value' + for k, v in search_opts.items(): + url = url + '&' + k + '=' + v + req = fakes.HTTPRequest.blank(url, use_admin_context=use_admin_context) + + shares = [ + {'id': 'id1', 'display_name': 'n1'}, + {'id': 'id2', 'display_name': 'n2'}, + {'id': 'id3', 'display_name': 'n3'}, + ] + self.mock_object(share_api.API, 'get_all', + mock.Mock(return_value=shares)) + + result = self.controller.index(req) + + search_opts_expected = { + 'display_name': search_opts['name'], + 'status': search_opts['status'], + 'share_server_id': search_opts['share_server_id'], + 'share_type_id': search_opts['share_type_id'], + 'snapshot_id': search_opts['snapshot_id'], + 'host': search_opts['host'], + 'share_network_id': search_opts['share_network_id'], + 'metadata': {'k1': 'v1'}, + 'extra_specs': {'k2': 'v2'}, + 'is_public': 'False', + } + if use_admin_context: + search_opts_expected.update({'fake_key': 'fake_value'}) + share_api.API.get_all.assert_called_once_with( + req.environ['manila.context'], + sort_key=search_opts['sort_key'], + sort_dir=search_opts['sort_dir'], + search_opts=search_opts_expected, + ) + self.assertEqual(1, len(result['shares'])) + self.assertEqual(shares[1]['id'], result['shares'][0]['id']) + self.assertEqual( + shares[1]['display_name'], result['shares'][0]['name']) + + def test_share_list_summary_with_search_opts_by_non_admin(self): + self._share_list_summary_with_search_opts(use_admin_context=False) + + def test_share_list_summary_with_search_opts_by_admin(self): + self._share_list_summary_with_search_opts(use_admin_context=True) + + def test_share_list_summary(self): + self.mock_object(share_api.API, 'get_all', + stubs.stub_share_get_all_by_project) + req = fakes.HTTPRequest.blank('/shares') + res_dict = self.controller.index(req) + expected = { + 'shares': [ + { + 'name': 'displayname', + 'id': '1', + 'links': [ + { + 'href': 'http://localhost/v1/fake/shares/1', + 'rel': 'self' + }, + { + 'href': 'http://localhost/fake/shares/1', + 'rel': 'bookmark' + } + ], + } + ] + } + self.assertEqual(expected, res_dict) + + def _share_list_detail_with_search_opts(self, use_admin_context): + search_opts = { + 'name': 'fake_name', + 'status': constants.STATUS_AVAILABLE, + 'share_server_id': 'fake_share_server_id', + 'share_type_id': 'fake_share_type_id', + 'snapshot_id': 'fake_snapshot_id', + 'host': 'fake_host', + 'share_network_id': 'fake_share_network_id', + 'metadata': '%7B%27k1%27%3A+%27v1%27%7D', # serialized k1=v1 + 'extra_specs': '%7B%27k2%27%3A+%27v2%27%7D', # serialized k2=v2 + 'sort_key': 'fake_sort_key', + 'sort_dir': 'fake_sort_dir', + 'limit': '1', + 'offset': '1', + 'is_public': 'False', + } + # fake_key should be filtered for non-admin + url = '/shares/detail?fake_key=fake_value' + for k, v in search_opts.items(): + url = url + '&' + k + '=' + v + req = fakes.HTTPRequest.blank(url, use_admin_context=use_admin_context) + + shares = [ + {'id': 'id1', 'display_name': 'n1'}, + { + 'id': 'id2', + 'display_name': 'n2', + 'status': constants.STATUS_AVAILABLE, + 'snapshot_id': 'fake_snapshot_id', + 'share_type_id': 'fake_share_type_id', + 'host': 'fake_host', + 'share_network_id': 'fake_share_network_id', + }, + {'id': 'id3', 'display_name': 'n3'}, + ] + self.mock_object(share_api.API, 'get_all', + mock.Mock(return_value=shares)) + + result = self.controller.detail(req) + + search_opts_expected = { + 'display_name': search_opts['name'], + 'status': search_opts['status'], + 'share_server_id': search_opts['share_server_id'], + 'share_type_id': search_opts['share_type_id'], + 'snapshot_id': search_opts['snapshot_id'], + 'host': search_opts['host'], + 'share_network_id': search_opts['share_network_id'], + 'metadata': {'k1': 'v1'}, + 'extra_specs': {'k2': 'v2'}, + 'is_public': 'False', + } + if use_admin_context: + search_opts_expected.update({'fake_key': 'fake_value'}) + share_api.API.get_all.assert_called_once_with( + req.environ['manila.context'], + sort_key=search_opts['sort_key'], + sort_dir=search_opts['sort_dir'], + search_opts=search_opts_expected, + ) + self.assertEqual(1, len(result['shares'])) + self.assertEqual(shares[1]['id'], result['shares'][0]['id']) + self.assertEqual( + shares[1]['display_name'], result['shares'][0]['name']) + self.assertEqual( + shares[1]['snapshot_id'], result['shares'][0]['snapshot_id']) + self.assertEqual( + shares[1]['status'], result['shares'][0]['status']) + self.assertEqual( + shares[1]['share_type_id'], result['shares'][0]['share_type']) + self.assertEqual( + shares[1]['snapshot_id'], result['shares'][0]['snapshot_id']) + self.assertEqual( + shares[1]['host'], result['shares'][0]['host']) + self.assertEqual( + shares[1]['share_network_id'], + result['shares'][0]['share_network_id']) + + def test_share_list_detail_with_search_opts_by_non_admin(self): + self._share_list_detail_with_search_opts(use_admin_context=False) + + def test_share_list_detail_with_search_opts_by_admin(self): + self._share_list_detail_with_search_opts(use_admin_context=True) + + def _list_detail_common_expected(self): + return { + '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', + 'snapshot_support': True, + '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, + 'links': [ + { + 'href': 'http://localhost/v1/fake/shares/1', + 'rel': 'self' + }, + { + 'href': 'http://localhost/fake/shares/1', + 'rel': 'bookmark' + } + ], + } + ] + } + + def _list_detail_test_common(self, req, expected): + self.mock_object(share_api.API, 'get_all', + stubs.stub_share_get_all_by_project) + res_dict = self.controller.detail(req) + self.assertEqual(expected, res_dict) + self.assertEqual(res_dict['shares'][0]['volume_type'], + res_dict['shares'][0]['share_type']) + + def test_share_list_detail(self): + env = {'QUERY_STRING': 'name=Share+Test+Name'} + req = fakes.HTTPRequest.blank('/shares/detail', environ=env) + expected = self._list_detail_common_expected() + expected['shares'][0].pop('snapshot_support') + self._list_detail_test_common(req, expected) + + def test_share_list_detail_with_consistency_group(self): + env = {'QUERY_STRING': 'name=Share+Test+Name'} + req = fakes.HTTPRequest.blank('/shares/detail', environ=env, + version="2.4") + expected = self._list_detail_common_expected() + expected['shares'][0]['consistency_group_id'] = None + expected['shares'][0]['source_cgsnapshot_member_id'] = None + self._list_detail_test_common(req, expected) + + def test_share_list_detail_with_task_state(self): + env = {'QUERY_STRING': 'name=Share+Test+Name'} + req = fakes.HTTPRequest.blank('/shares/detail', environ=env, + version="2.5") + expected = self._list_detail_common_expected() + expected['shares'][0]['consistency_group_id'] = None + expected['shares'][0]['source_cgsnapshot_member_id'] = None + expected['shares'][0]['task_state'] = None + self._list_detail_test_common(req, expected) + + def test_remove_invalid_options(self): + ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False) + search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'} + expected_opts = {'a': 'a', 'c': 'c'} + allowed_opts = ['a', 'c'] + common.remove_invalid_options(ctx, search_opts, allowed_opts) + self.assertEqual(expected_opts, search_opts) + + def test_remove_invalid_options_admin(self): + ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=True) + search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'} + expected_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'} + allowed_opts = ['a', 'c'] + common.remove_invalid_options(ctx, search_opts, allowed_opts) + self.assertEqual(expected_opts, search_opts) + + +def _fake_access_get(self, ctxt, access_id): + + class Access(object): + def __init__(self, **kwargs): + self.STATE_NEW = 'fake_new' + self.STATE_ACTIVE = 'fake_active' + self.STATE_ERROR = 'fake_error' + self.params = kwargs + self.params['state'] = self.STATE_NEW + self.share_id = kwargs.get('share_id') + self.id = access_id + + def __getitem__(self, item): + return self.params[item] + + access = Access(access_id=access_id, share_id='fake_share_id') + return access + + +@ddt.ddt +class ShareActionsTest(test.TestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.controller = shares.ShareController() + self.mock_object(share_api.API, 'get', stubs.stub_share_get) + + @ddt.data( + {'access_type': 'ip', 'access_to': '127.0.0.1'}, + {'access_type': 'user', 'access_to': '1' * 4}, + {'access_type': 'user', 'access_to': '1' * 32}, + {'access_type': 'user', 'access_to': 'fake\\]{.-_\'`;}['}, + {'access_type': 'user', 'access_to': 'MYDOMAIN\\Administrator'}, + {'access_type': 'cert', 'access_to': 'x'}, + {'access_type': 'cert', 'access_to': 'tenant.example.com'}, + {'access_type': 'cert', 'access_to': 'x' * 64}, + ) + def test_allow_access(self, access): + self.mock_object(share_api.API, + 'allow_access', + mock.Mock(return_value={'fake': 'fake'})) + + id = 'fake_share_id' + body = {'os-allow_access': access} + expected = {'access': {'fake': 'fake'}} + req = fakes.HTTPRequest.blank('/v1/tenant1/shares/%s/action' % id) + res = self.controller._allow_access(req, id, body) + self.assertEqual(expected, res) + + @ddt.data( + {'access_type': 'error_type', 'access_to': '127.0.0.1'}, + {'access_type': 'ip', 'access_to': 'localhost'}, + {'access_type': 'ip', 'access_to': '127.0.0.*'}, + {'access_type': 'ip', 'access_to': '127.0.0.0/33'}, + {'access_type': 'ip', 'access_to': '127.0.0.256'}, + {'access_type': 'user', 'access_to': '1'}, + {'access_type': 'user', 'access_to': '1' * 3}, + {'access_type': 'user', 'access_to': '1' * 33}, + {'access_type': 'user', 'access_to': 'root^'}, + {'access_type': 'cert', 'access_to': ''}, + {'access_type': 'cert', 'access_to': ' '}, + {'access_type': 'cert', 'access_to': 'x' * 65}, + ) + def test_allow_access_error(self, access): + id = 'fake_share_id' + body = {'os-allow_access': access} + req = fakes.HTTPRequest.blank('/v1/tenant1/shares/%s/action' % id) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._allow_access, req, id, body) + + def test_deny_access(self): + def _stub_deny_access(*args, **kwargs): + pass + + self.mock_object(share_api.API, "deny_access", _stub_deny_access) + self.mock_object(share_api.API, "access_get", _fake_access_get) + + id = 'fake_share_id' + body = {"os-deny_access": {"access_id": 'fake_acces_id'}} + req = fakes.HTTPRequest.blank('/v1/tenant1/shares/%s/action' % id) + res = self.controller._deny_access(req, id, body) + self.assertEqual(202, res.status_int) + + def test_deny_access_not_found(self): + def _stub_deny_access(*args, **kwargs): + pass + + self.mock_object(share_api.API, "deny_access", _stub_deny_access) + self.mock_object(share_api.API, "access_get", _fake_access_get) + + id = 'super_fake_share_id' + body = {"os-deny_access": {"access_id": 'fake_acces_id'}} + req = fakes.HTTPRequest.blank('/v1/tenant1/shares/%s/action' % id) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._deny_access, + req, + id, + body) + + def test_access_list(self): + def _fake_access_get_all(*args, **kwargs): + return [{"state": "fakestatus", + "id": "fake_share_id", + "access_type": "fakeip", + "access_to": "127.0.0.1"}] + + self.mock_object(share_api.API, "access_get_all", + _fake_access_get_all) + id = 'fake_share_id' + body = {"os-access_list": None} + req = fakes.HTTPRequest.blank('/v1/tenant1/shares/%s/action' % id) + res_dict = self.controller._access_list(req, id, body) + expected = _fake_access_get_all() + self.assertEqual(expected, res_dict['access_list']) + + def test_extend(self): + id = 'fake_share_id' + share = stubs.stub_share_get(None, None, id) + self.mock_object(share_api.API, 'get', mock.Mock(return_value=share)) + self.mock_object(share_api.API, "extend") + + size = '123' + body = {"os-extend": {'new_size': size}} + req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id) + + actual_response = self.controller._extend(req, id, body) + + share_api.API.get.assert_called_once_with(mock.ANY, id) + share_api.API.extend.assert_called_once_with( + mock.ANY, share, int(size)) + self.assertEqual(202, actual_response.status_int) + + @ddt.data({"os-extend": ""}, + {"os-extend": {"new_size": "foo"}}, + {"os-extend": {"new_size": {'foo': 'bar'}}}) + def test_extend_invalid_body(self, body): + id = 'fake_share_id' + req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._extend, req, id, body) + + @ddt.data({'source': exception.InvalidInput, + 'target': webob.exc.HTTPBadRequest}, + {'source': exception.InvalidShare, + 'target': webob.exc.HTTPBadRequest}, + {'source': exception.ShareSizeExceedsAvailableQuota, + 'target': webob.exc.HTTPForbidden}) + @ddt.unpack + def test_extend_exception(self, source, target): + id = 'fake_share_id' + req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id) + body = {"os-extend": {'new_size': '123'}} + self.mock_object(share_api.API, "extend", + mock.Mock(side_effect=source('fake'))) + + self.assertRaises(target, self.controller._extend, req, id, body) + + def test_shrink(self): + id = 'fake_share_id' + share = stubs.stub_share_get(None, None, id) + self.mock_object(share_api.API, 'get', mock.Mock(return_value=share)) + self.mock_object(share_api.API, "shrink") + + size = '123' + body = {"os-shrink": {'new_size': size}} + req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id) + + actual_response = self.controller._shrink(req, id, body) + + share_api.API.get.assert_called_once_with(mock.ANY, id) + share_api.API.shrink.assert_called_once_with( + mock.ANY, share, int(size)) + self.assertEqual(202, actual_response.status_int) + + @ddt.data({"os-shrink": ""}, + {"os-shrink": {"new_size": "foo"}}, + {"os-shrink": {"new_size": {'foo': 'bar'}}}) + def test_shrink_invalid_body(self, body): + id = 'fake_share_id' + req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._shrink, req, id, body) + + @ddt.data({'source': exception.InvalidInput, + 'target': webob.exc.HTTPBadRequest}, + {'source': exception.InvalidShare, + 'target': webob.exc.HTTPBadRequest}) + @ddt.unpack + def test_shrink_exception(self, source, target): + id = 'fake_share_id' + req = fakes.HTTPRequest.blank('/v1/shares/%s/action' % id) + body = {"os-shrink": {'new_size': '123'}} + self.mock_object(share_api.API, "shrink", + mock.Mock(side_effect=source('fake'))) + + self.assertRaises(target, self.controller._shrink, req, id, body) + + +@ddt.ddt +class ShareAdminActionsAPITest(test.TestCase): + + def setUp(self): + super(self.__class__, self).setUp() + CONF.set_default("default_share_type", None) + self.flags(rpc_backend='manila.openstack.common.rpc.impl_fake') + self.share_api = share_api.API() + self.admin_context = context.RequestContext('admin', 'fake', True) + self.member_context = context.RequestContext('fake', 'fake') + + def _get_context(self, role): + return getattr(self, '%s_context' % role) + + def _setup_share_data(self, share=None, version='2.7'): + if share is None: + share = db_utils.create_share(status=constants.STATUS_AVAILABLE, + size='1', + override_defaults=True) + req = fakes.HTTPRequest.blank( + '/v2/fake/shares/%s/action' % share['id'], version=version) + return share, req + + def _reset_status(self, ctxt, model, req, db_access_method, + valid_code, valid_status=None, body=None, version='2.7'): + if float(version) > 2.6: + action_name = 'reset_status' + else: + action_name = 'os-reset_status' + if body is None: + body = {action_name: {'status': constants.STATUS_ERROR}} + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.headers['X-Openstack-Manila-Api-Version'] = version + req.body = six.b(jsonutils.dumps(body)) + req.environ['manila.context'] = ctxt + + resp = req.get_response(fakes.app()) + + # validate response code and model status + self.assertEqual(valid_code, resp.status_int) + + if valid_code == 404: + self.assertRaises(exception.NotFound, + db_access_method, + ctxt, + model['id']) + else: + actual_model = db_access_method(ctxt, model['id']) + self.assertEqual(valid_status, actual_model['status']) + + @ddt.data(*fakes.fixture_reset_status_with_different_roles) + @ddt.unpack + def test_share_reset_status_with_different_roles(self, role, valid_code, + valid_status, version): + share, req = self._setup_share_data(version=version) + ctxt = self._get_context(role) + + self._reset_status(ctxt, share, req, db.share_get, valid_code, + valid_status, version=version) + + @ddt.data(*fakes.fixture_invalid_reset_status_body) + def test_share_invalid_reset_status_body(self, body): + share, req = self._setup_share_data(version='2.6') + ctxt = self.admin_context + + self._reset_status(ctxt, share, req, db.share_get, 400, + constants.STATUS_AVAILABLE, body, version='2.6') + + @ddt.data('2.6', '2.7') + def test_share_reset_status_for_missing(self, version): + fake_share = {'id': 'missing-share-id'} + req = fakes.HTTPRequest.blank( + '/v2/fake/shares/%s/action' % fake_share['id'], version=version) + + self._reset_status(self.admin_context, fake_share, req, + db.share_snapshot_get, 404, version=version) + + def _force_delete(self, ctxt, model, req, db_access_method, valid_code, + check_model_in_db=False, version='2.7'): + if float(version) > 2.6: + action_name = 'force_delete' + else: + action_name = 'os-force_delete' + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.headers['X-Openstack-Manila-Api-Version'] = version + req.body = six.b(jsonutils.dumps({action_name: {}})) + req.environ['manila.context'] = ctxt + + resp = req.get_response(fakes.app()) + + # validate response + self.assertEqual(valid_code, resp.status_int) + + if valid_code == 202 and check_model_in_db: + self.assertRaises(exception.NotFound, + db_access_method, + ctxt, + model['id']) + + @ddt.data(*fakes.fixture_force_delete_with_different_roles) + @ddt.unpack + def test_share_force_delete_with_different_roles(self, role, resp_code, + version): + share, req = self._setup_share_data(version=version) + ctxt = self._get_context(role) + + self._force_delete(ctxt, share, req, db.share_get, resp_code, + check_model_in_db=True, version=version) + + @ddt.data('2.6', '2.7') + def test_share_force_delete_missing(self, version): + share, req = self._setup_share_data( + share={'id': 'fake'}, version=version) + ctxt = self._get_context('admin') + + self._force_delete( + ctxt, share, req, db.share_get, 404, version=version) + + +@ddt.ddt +class ShareUnmanageTest(test.TestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.controller = shares.ShareController() + self.mock_object(share_api.API, 'get_all', + stubs.stub_get_all_shares) + self.mock_object(share_api.API, 'get', + stubs.stub_share_get) + self.mock_object(share_api.API, 'update', stubs.stub_share_update) + self.mock_object(share_api.API, 'delete', stubs.stub_share_delete) + self.mock_object(share_api.API, 'get_snapshot', + stubs.stub_snapshot_get) + self.share_id = 'fake' + self.request = fakes.HTTPRequest.blank( + '/share/%s/unmanage' % self.share_id, + use_admin_context=True, version='2.7', + ) + + def test_unmanage_share(self): + share = dict(status=constants.STATUS_AVAILABLE, id='foo_id') + self.mock_object(share_api.API, 'get', mock.Mock(return_value=share)) + self.mock_object(share_api.API, 'unmanage', mock.Mock()) + self.mock_object( + self.controller.share_api.db, 'share_snapshot_get_all_for_share', + mock.Mock(return_value=[])) + + actual_result = self.controller.unmanage(self.request, share['id']) + + self.assertEqual(202, actual_result.status_int) + self.controller.share_api.db.share_snapshot_get_all_for_share.\ + assert_called_once_with( + self.request.environ['manila.context'], share['id']) + self.controller.share_api.get.assert_called_once_with( + self.request.environ['manila.context'], share['id']) + share_api.API.unmanage.assert_called_once_with( + self.request.environ['manila.context'], share) + + def test_unmanage_share_that_has_snapshots(self): + share = dict(status=constants.STATUS_AVAILABLE, id='foo_id') + snapshots = ['foo', 'bar'] + self.mock_object(self.controller.share_api, 'unmanage') + self.mock_object( + self.controller.share_api.db, 'share_snapshot_get_all_for_share', + mock.Mock(return_value=snapshots)) + self.mock_object( + self.controller.share_api, 'get', + mock.Mock(return_value=share)) + + self.assertRaises( + webob.exc.HTTPForbidden, + self.controller.unmanage, self.request, share['id']) + + self.assertFalse(self.controller.share_api.unmanage.called) + self.controller.share_api.db.share_snapshot_get_all_for_share.\ + assert_called_once_with( + self.request.environ['manila.context'], share['id']) + self.controller.share_api.get.assert_called_once_with( + self.request.environ['manila.context'], share['id']) + + def test_unmanage_share_based_on_share_server(self): + share = dict(share_server_id='foo_id', id='bar_id') + self.mock_object( + self.controller.share_api, 'get', + mock.Mock(return_value=share)) + + self.assertRaises( + webob.exc.HTTPForbidden, + self.controller.unmanage, self.request, share['id']) + + self.controller.share_api.get.assert_called_once_with( + self.request.environ['manila.context'], share['id']) + + @ddt.data(*constants.TRANSITIONAL_STATUSES) + def test_unmanage_share_with_transitional_state(self, share_status): + share = dict(status=share_status, id='foo_id') + self.mock_object( + self.controller.share_api, 'get', + mock.Mock(return_value=share)) + + self.assertRaises( + webob.exc.HTTPForbidden, + self.controller.unmanage, self.request, share['id']) + + self.controller.share_api.get.assert_called_once_with( + self.request.environ['manila.context'], share['id']) + + def test_unmanage_share_not_found(self): + self.mock_object(share_api.API, 'get', mock.Mock( + side_effect=exception.NotFound)) + self.mock_object(share_api.API, 'unmanage', mock.Mock()) + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.unmanage, + self.request, self.share_id) + + @ddt.data(exception.InvalidShare(reason="fake"), + exception.PolicyNotAuthorized(action="fake"),) + def test_unmanage_share_invalid(self, side_effect): + share = dict(status=constants.STATUS_AVAILABLE, id='foo_id') + self.mock_object(share_api.API, 'get', mock.Mock(return_value=share)) + self.mock_object(share_api.API, 'unmanage', mock.Mock( + side_effect=side_effect)) + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.unmanage, + self.request, self.share_id) + + def test_wrong_permissions(self): + share_id = 'fake' + req = fakes.HTTPRequest.blank('/share/%s/unmanage' % share_id, + use_admin_context=False, version='2.7') + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.unmanage, + req, + share_id) + + def test_unsupported_version(self): + share_id = 'fake' + req = fakes.HTTPRequest.blank('/share/%s/unmanage' % share_id, + use_admin_context=False, version='2.6') + + self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.unmanage, + req, + share_id) + + +def get_fake_manage_body(export_path='/fake', service_host='fake@host#POOL', + protocol='fake', share_type='fake', **kwargs): + fake_share = { + 'export_path': export_path, + 'service_host': service_host, + 'protocol': protocol, + 'share_type': share_type, + } + fake_share.update(kwargs) + return {'share': fake_share} + + +@ddt.ddt +class ShareManageTest(test.TestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.controller = shares.ShareController() + self.request = fakes.HTTPRequest.blank( + '/v2/share/manage', use_admin_context=True, version='2.7') + + def _setup_manage_mocks(self, service_is_up=True): + self.mock_object(db, 'service_get_by_host_and_topic', mock.Mock( + return_value={'host': 'fake'})) + self.mock_object(share_types, 'get_share_type_by_name_or_id', + mock.Mock(return_value={'id': 'fake'})) + self.mock_object(utils, 'service_is_up', mock.Mock( + return_value=service_is_up)) + if service_is_up: + self.mock_object(utils, 'validate_service_host') + else: + self.mock_object( + utils, + 'validate_service_host', + mock.Mock(side_effect=exception.ServiceIsDown(service='fake'))) + + @ddt.data({}, + {'share': None}, + {'shares': {}}, + {'share': get_fake_manage_body('', None, None)}) + def test_share_manage_invalid_body(self, body): + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.manage, + self.request, + body) + + def test_share_manage_service_not_found(self): + body = get_fake_manage_body() + self.mock_object(db, 'service_get_by_host_and_topic', mock.Mock( + side_effect=exception.ServiceNotFound(service_id='fake'))) + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.manage, + self.request, + body) + + def test_share_manage_share_type_not_found(self): + body = get_fake_manage_body() + self.mock_object(db, 'service_get_by_host_and_topic', mock.Mock()) + self.mock_object(utils, 'service_is_up', mock.Mock(return_value=True)) + self.mock_object(db, 'share_type_get_by_name', mock.Mock( + side_effect=exception.ShareTypeNotFoundByName( + share_type_name='fake'))) + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.manage, + self.request, + body) + + @ddt.data({'service_is_up': False, 'service_host': 'fake@host#POOL'}, + {'service_is_up': True, 'service_host': 'fake@host'}) + def test_share_manage_bad_request(self, settings): + body = get_fake_manage_body(service_host=settings.pop('service_host')) + self._setup_manage_mocks(**settings) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.manage, + self.request, + body) + + def test_share_manage_duplicate_share(self): + body = get_fake_manage_body() + self._setup_manage_mocks() + self.mock_object(share_api.API, 'manage', + mock.Mock(side_effect=exception.ManilaException())) + + self.assertRaises(webob.exc.HTTPConflict, + self.controller.manage, + self.request, + body) + + def test_share_manage_forbidden_manage(self): + body = get_fake_manage_body() + self._setup_manage_mocks() + error = mock.Mock(side_effect=exception.PolicyNotAuthorized(action='')) + self.mock_object(share_api.API, 'manage', error) + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.manage, + self.request, + body) + + def test_share_manage_forbidden_validate_service_host(self): + body = get_fake_manage_body() + self._setup_manage_mocks() + error = mock.Mock(side_effect=exception.PolicyNotAuthorized(action='')) + self.mock_object( + utils, 'validate_service_host', mock.Mock(side_effect=error)) + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.manage, + self.request, + body) + + @ddt.data( + get_fake_manage_body(name='foo', description='bar'), + get_fake_manage_body(display_name='foo', description='bar'), + get_fake_manage_body(name='foo', display_description='bar'), + get_fake_manage_body(display_name='foo', display_description='bar'), + get_fake_manage_body(display_name='foo', display_description='bar', + driver_options=dict(volume_id='quuz')), + ) + def test_share_manage(self, data): + self._setup_manage_mocks() + return_share = {'share_type_id': '', 'id': 'fake'} + self.mock_object( + share_api.API, 'manage', mock.Mock(return_value=return_share)) + share = { + 'host': data['share']['service_host'], + 'export_location': data['share']['export_path'], + 'share_proto': data['share']['protocol'].upper(), + 'share_type_id': 'fake', + 'display_name': 'foo', + 'display_description': 'bar', + } + driver_options = data['share'].get('driver_options', {}) + + actual_result = self.controller.manage(self.request, data) + + share_api.API.manage.assert_called_once_with( + mock.ANY, share, driver_options) + self.assertIsNotNone(actual_result) + + def test_wrong_permissions(self): + body = get_fake_manage_body() + + self.assertRaises( + webob.exc.HTTPForbidden, + self.controller.manage, + fakes.HTTPRequest.blank( + '/share/manage', use_admin_context=False, version='2.7'), + body, + ) + + def test_unsupported_version(self): + share_id = 'fake' + req = fakes.HTTPRequest.blank( + '/share/manage', use_admin_context=False, version='2.6') + + self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.manage, + req, + share_id) diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index 22a2e3c8b9..63b3211fba 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -36,7 +36,7 @@ ShareGroup = [ help="The minimum api microversion is configured to be the " "value of the minimum microversion supported by Manila."), cfg.StrOpt("max_api_microversion", - default="2.6", + default="2.7", help="The maximum api microversion is configured to be the " "value of the latest microversion supported by Manila."), cfg.StrOpt("region", diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py index b61d526fb5..877a82f526 100644 --- a/manila_tempest_tests/services/share/v2/json/shares_client.py +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -96,14 +96,20 @@ class SharesV2Client(shares_client.SharesClient): return super(SharesV2Client, self).copy(url, headers=headers) def reset_state(self, s_id, status="error", s_type="shares", - headers=None, version=LATEST_MICROVERSION): + headers=None, version=LATEST_MICROVERSION, + action_name=None): """Resets the state of a share, snapshot, cg, or a cgsnapshot. status: available, error, creating, deleting, error_deleting s_type: shares, share_instances, snapshots, consistency-groups, cgsnapshots. """ - body = {"os-reset_status": {"status": status}} + if action_name is None: + if float(version) > 2.6: + action_name = 'reset_status' + else: + action_name = 'os-reset_status' + body = {action_name: {"status": status}} body = json.dumps(body) resp, body = self.post("%s/%s/action" % (s_type, s_id), body, headers=headers, extra_headers=True, @@ -112,12 +118,17 @@ class SharesV2Client(shares_client.SharesClient): return body def force_delete(self, s_id, s_type="shares", headers=None, - version=LATEST_MICROVERSION): + version=LATEST_MICROVERSION, action_name=None): """Force delete share or snapshot. s_type: shares, snapshots """ - body = {"os-force_delete": None} + if action_name is None: + if float(version) > 2.6: + action_name = 'force_delete' + else: + action_name = 'os-force_delete' + body = {action_name: None} body = json.dumps(body) resp, body = self.post("%s/%s/action" % (s_type, s_id), body, headers=headers, extra_headers=True, @@ -276,6 +287,267 @@ class SharesV2Client(shares_client.SharesClient): (instance_id, status, self.build_timeout)) raise exceptions.TimeoutException(message) +############### + + def extend_share(self, share_id, new_size, version=LATEST_MICROVERSION, + action_name=None): + if action_name is None: + action_name = 'extend' if float(version) > 2.6 else 'os-extend' + post_body = { + action_name: { + "new_size": new_size, + } + } + body = json.dumps(post_body) + resp, body = self.post( + "shares/%s/action" % share_id, body, version=version) + self.expected_success(202, resp.status) + return body + + def shrink_share(self, share_id, new_size, version=LATEST_MICROVERSION, + action_name=None): + if action_name is None: + action_name = 'shrink' if float(version) > 2.6 else 'os-shrnk' + post_body = { + action_name: { + "new_size": new_size, + } + } + body = json.dumps(post_body) + resp, body = self.post( + "shares/%s/action" % share_id, body, version=version) + self.expected_success(202, resp.status) + return body + +############### + + def manage_share(self, service_host, protocol, export_path, + share_type_id, name=None, description=None, + version=LATEST_MICROVERSION, url=None): + post_body = { + "share": { + "export_path": export_path, + "service_host": service_host, + "protocol": protocol, + "share_type": share_type_id, + "name": name, + "description": description, + } + } + if url is None: + if float(version) > 2.6: + url = 'shares/manage' + else: + url = 'os-share-manage' + body = json.dumps(post_body) + resp, body = self.post(url, body, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def unmanage_share(self, share_id, version=LATEST_MICROVERSION, url=None, + action_name=None, body=None): + if url is None: + url = 'shares' if float(version) > 2.6 else 'os-share-unmanage' + if action_name is None: + action_name = 'action' if float(version) > 2.6 else 'unmanage' + if body is None and float(version) > 2.6: + body = json.dumps({'unmanage': {}}) + resp, body = self.post( + "%(url)s/%(share_id)s/%(action_name)s" % { + 'url': url, 'share_id': share_id, 'action_name': action_name}, + body, + version=version) + self.expected_success(202, resp.status) + return body + +############### + + def _get_access_action_name(self, version): + if float(version) > 2.6: + return 'allow_access' + return 'os-allow_access' + + def create_access_rule(self, share_id, access_type="ip", + access_to="0.0.0.0", access_level=None, + version=LATEST_MICROVERSION, action_name=None): + post_body = { + self._get_access_action_name(version): { + "access_type": access_type, + "access_to": access_to, + "access_level": access_level, + } + } + body = json.dumps(post_body) + resp, body = self.post( + "shares/%s/action" % share_id, body, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def list_access_rules(self, share_id, version=LATEST_MICROVERSION, + action_name=None): + body = {self._get_access_action_name(version): None} + resp, body = self.post( + "shares/%s/action" % share_id, json.dumps(body), version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def delete_access_rule(self, share_id, rule_id, + version=LATEST_MICROVERSION, action_name=None): + post_body = { + self._get_access_action_name(version): { + "access_id": rule_id, + } + } + body = json.dumps(post_body) + resp, body = self.post( + "shares/%s/action" % share_id, body, version=version) + self.expected_success(202, resp.status) + return body + +############### + + def list_availability_zones(self, url='availability-zones', + version=LATEST_MICROVERSION): + """Get list of availability zones.""" + if url is None: + if float(version) > 2.6: + url = 'availability-zones' + else: + url = 'os-availability-zone' + resp, body = self.get(url, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + +############### + + def list_services(self, params=None, url=None, + version=LATEST_MICROVERSION): + """List services.""" + if url is None: + url = 'services' if float(version) > 2.6 else 'os-services' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + +############### + + def list_share_types(self, params=None, version=LATEST_MICROVERSION): + uri = 'types' + if params is not None: + uri += '?%s' % urllib.urlencode(params) + resp, body = self.get(uri, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def create_share_type(self, name, is_public=True, + version=LATEST_MICROVERSION, **kwargs): + if float(version) > 2.6: + is_public_keyname = 'share_type_access:is_public' + else: + is_public_keyname = 'os-share-type-access:is_public' + post_body = { + 'name': name, + 'extra_specs': kwargs.get('extra_specs'), + is_public_keyname: is_public, + } + post_body = json.dumps({'share_type': post_body}) + resp, body = self.post('types', post_body, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def delete_share_type(self, share_type_id, version=LATEST_MICROVERSION): + resp, body = self.delete("types/%s" % share_type_id, version=version) + self.expected_success(202, resp.status) + return body + + def get_share_type(self, share_type_id, version=LATEST_MICROVERSION): + resp, body = self.get("types/%s" % share_type_id, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def list_access_to_share_type(self, share_type_id, + version=LATEST_MICROVERSION, + action_name=None): + if action_name is None: + if float(version) > 2.6: + action_name = 'share_type_access' + else: + action_name = 'os-share-type-access' + url = 'types/%(st_id)s/%(action_name)s' % { + 'st_id': share_type_id, 'action_name': action_name} + resp, body = self.get(url, version=version) + # [{"share_type_id": "%st_id%", "project_id": "%project_id%"}, ] + self.expected_success(200, resp.status) + return self._parse_resp(body) + +############### + + def _get_quotas_url(self, version): + if float(version) > 2.6: + return 'quota-sets' + return 'os-quota-sets' + + def default_quotas(self, tenant_id, url=None, version=LATEST_MICROVERSION): + if url is None: + url = self._get_quotas_url(version) + url += '/%s' % tenant_id + resp, body = self.get("%s/defaults" % url, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def show_quotas(self, tenant_id, user_id=None, url=None, + version=LATEST_MICROVERSION): + if url is None: + url = self._get_quotas_url(version) + url += '/%s' % tenant_id + if user_id is not None: + url += "?user_id=%s" % user_id + resp, body = self.get(url, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def reset_quotas(self, tenant_id, user_id=None, url=None, + version=LATEST_MICROVERSION): + if url is None: + url = self._get_quotas_url(version) + url += '/%s' % tenant_id + if user_id is not None: + url += "?user_id=%s" % user_id + resp, body = self.delete(url, version=version) + self.expected_success(202, resp.status) + return body + + def update_quotas(self, tenant_id, user_id=None, shares=None, + snapshots=None, gigabytes=None, snapshot_gigabytes=None, + share_networks=None, force=True, url=None, + version=LATEST_MICROVERSION): + if url is None: + url = self._get_quotas_url(version) + url += '/%s' % tenant_id + if user_id is not None: + url += "?user_id=%s" % user_id + + put_body = {"tenant_id": tenant_id} + if force: + put_body["force"] = "true" + if shares is not None: + put_body["shares"] = shares + if snapshots is not None: + put_body["snapshots"] = snapshots + if gigabytes is not None: + put_body["gigabytes"] = gigabytes + if snapshot_gigabytes is not None: + put_body["snapshot_gigabytes"] = snapshot_gigabytes + if share_networks is not None: + put_body["share_networks"] = share_networks + put_body = json.dumps({"quota_set": put_body}) + + resp, body = self.put(url, put_body, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + ############### def create_consistency_group(self, name=None, description=None, @@ -481,9 +753,15 @@ class SharesV2Client(shares_client.SharesClient): ############### - def migrate_share(self, share_id, host, version=LATEST_MICROVERSION): + def migrate_share(self, share_id, host, version=LATEST_MICROVERSION, + action_name=None): + if action_name is None: + if float(version) > 2.6: + action_name = 'migrate_share' + else: + action_name = 'os-migrate_share' post_body = { - 'os-migrate_share': { + action_name: { 'host': host, } } diff --git a/manila_tempest_tests/tests/api/admin/test_quotas.py b/manila_tempest_tests/tests/api/admin/test_quotas.py index dd7164eb3b..d8aa4d6746 100644 --- a/manila_tempest_tests/tests/api/admin/test_quotas.py +++ b/manila_tempest_tests/tests/api/admin/test_quotas.py @@ -28,12 +28,12 @@ class SharesAdminQuotasTest(base.BaseSharesAdminTest): def resource_setup(cls): cls.os = clients.AdminManager() super(SharesAdminQuotasTest, cls).resource_setup() - cls.user_id = cls.shares_client.user_id - cls.tenant_id = cls.shares_client.tenant_id + cls.user_id = cls.shares_v2_client.user_id + cls.tenant_id = cls.shares_v2_client.tenant_id @test.attr(type=["gate", "smoke", ]) def test_default_quotas(self): - quotas = self.shares_client.default_quotas(self.tenant_id) + quotas = self.shares_v2_client.default_quotas(self.tenant_id) self.assertGreater(int(quotas["gigabytes"]), -2) self.assertGreater(int(quotas["snapshot_gigabytes"]), -2) self.assertGreater(int(quotas["shares"]), -2) @@ -42,7 +42,7 @@ class SharesAdminQuotasTest(base.BaseSharesAdminTest): @test.attr(type=["gate", "smoke", ]) def test_show_quotas(self): - quotas = self.shares_client.show_quotas(self.tenant_id) + quotas = self.shares_v2_client.show_quotas(self.tenant_id) self.assertGreater(int(quotas["gigabytes"]), -2) self.assertGreater(int(quotas["snapshot_gigabytes"]), -2) self.assertGreater(int(quotas["shares"]), -2) @@ -51,7 +51,8 @@ class SharesAdminQuotasTest(base.BaseSharesAdminTest): @test.attr(type=["gate", "smoke", ]) def test_show_quotas_for_user(self): - quotas = self.shares_client.show_quotas(self.tenant_id, self.user_id) + quotas = self.shares_v2_client.show_quotas( + self.tenant_id, self.user_id) self.assertGreater(int(quotas["gigabytes"]), -2) self.assertGreater(int(quotas["snapshot_gigabytes"]), -2) self.assertGreater(int(quotas["shares"]), -2) @@ -62,159 +63,145 @@ class SharesAdminQuotasTest(base.BaseSharesAdminTest): class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest): force_tenant_isolation = True + client_version = '2' + + def setUp(self): + super(self.__class__, self).setUp() + self.client = self.get_client_with_isolated_creds( + client_version=self.client_version) + self.tenant_id = self.client.tenant_id + self.user_id = self.client.user_id @test.attr(type=["gate", "smoke", ]) def test_update_tenant_quota_shares(self): - client = self.get_client_with_isolated_creds() - # get current quotas - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) new_quota = int(quotas["shares"]) + 2 # set new quota for shares - updated = client.update_quotas(client.tenant_id, shares=new_quota) + updated = self.client.update_quotas(self.tenant_id, shares=new_quota) self.assertEqual(int(updated["shares"]), new_quota) @test.attr(type=["gate", "smoke", ]) def test_update_user_quota_shares(self): - client = self.get_client_with_isolated_creds() - # get current quotas - quotas = client.show_quotas(client.tenant_id, client.user_id) + quotas = self.client.show_quotas(self.tenant_id, self.user_id) new_quota = int(quotas["shares"]) - 1 # set new quota for shares - updated = client.update_quotas( - client.tenant_id, client.user_id, shares=new_quota) + updated = self.client.update_quotas( + self.tenant_id, self.user_id, shares=new_quota) self.assertEqual(int(updated["shares"]), new_quota) @test.attr(type=["gate", "smoke", ]) def test_update_tenant_quota_snapshots(self): - client = self.get_client_with_isolated_creds() - # get current quotas - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) new_quota = int(quotas["snapshots"]) + 2 # set new quota for snapshots - updated = client.update_quotas(client.tenant_id, snapshots=new_quota) + updated = self.client.update_quotas( + self.tenant_id, snapshots=new_quota) self.assertEqual(int(updated["snapshots"]), new_quota) @test.attr(type=["gate", "smoke", ]) def test_update_user_quota_snapshots(self): - client = self.get_client_with_isolated_creds() - # get current quotas - quotas = client.show_quotas(client.tenant_id, client.user_id) + quotas = self.client.show_quotas(self.tenant_id, self.user_id) new_quota = int(quotas["snapshots"]) - 1 # set new quota for snapshots - updated = client.update_quotas( - client.tenant_id, client.user_id, snapshots=new_quota) + updated = self.client.update_quotas( + self.tenant_id, self.user_id, snapshots=new_quota) self.assertEqual(int(updated["snapshots"]), new_quota) @test.attr(type=["gate", "smoke", ]) def test_update_tenant_quota_gigabytes(self): - client = self.get_client_with_isolated_creds() - # get current quotas - custom = client.show_quotas(client.tenant_id) + custom = self.client.show_quotas(self.tenant_id) # make quotas for update gigabytes = int(custom["gigabytes"]) + 2 # set new quota for shares - updated = client.update_quotas( - client.tenant_id, gigabytes=gigabytes) + updated = self.client.update_quotas( + self.tenant_id, gigabytes=gigabytes) self.assertEqual(int(updated["gigabytes"]), gigabytes) @test.attr(type=["gate", "smoke", ]) def test_update_tenant_quota_snapshot_gigabytes(self): - client = self.get_client_with_isolated_creds() - # get current quotas - custom = client.show_quotas(client.tenant_id) + custom = self.client.show_quotas(self.tenant_id) # make quotas for update snapshot_gigabytes = int(custom["snapshot_gigabytes"]) + 2 # set new quota for shares - updated = client.update_quotas( - client.tenant_id, + updated = self.client.update_quotas( + self.tenant_id, snapshot_gigabytes=snapshot_gigabytes) self.assertEqual( int(updated["snapshot_gigabytes"]), snapshot_gigabytes) @test.attr(type=["gate", "smoke", ]) def test_update_user_quota_gigabytes(self): - client = self.get_client_with_isolated_creds() - # get current quotas - custom = client.show_quotas(client.tenant_id, client.user_id) + custom = self.client.show_quotas(self.tenant_id, self.user_id) # make quotas for update gigabytes = int(custom["gigabytes"]) - 1 # set new quota for shares - updated = client.update_quotas( - client.tenant_id, client.user_id, - gigabytes=gigabytes) + updated = self.client.update_quotas( + self.tenant_id, self.user_id, gigabytes=gigabytes) self.assertEqual(int(updated["gigabytes"]), gigabytes) @test.attr(type=["gate", "smoke", ]) def test_update_user_quota_snapshot_gigabytes(self): - client = self.get_client_with_isolated_creds() - # get current quotas - custom = client.show_quotas(client.tenant_id, client.user_id) + custom = self.client.show_quotas(self.tenant_id, self.user_id) # make quotas for update snapshot_gigabytes = int(custom["snapshot_gigabytes"]) - 1 # set new quota for shares - updated = client.update_quotas( - client.tenant_id, client.user_id, + updated = self.client.update_quotas( + self.tenant_id, self.user_id, snapshot_gigabytes=snapshot_gigabytes) self.assertEqual( int(updated["snapshot_gigabytes"]), snapshot_gigabytes) @test.attr(type=["gate", "smoke", ]) def test_update_tenant_quota_share_networks(self): - client = self.get_client_with_isolated_creds() - # get current quotas - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) new_quota = int(quotas["share_networks"]) + 2 # set new quota for share-networks - updated = client.update_quotas( - client.tenant_id, share_networks=new_quota) + updated = self.client.update_quotas( + self.tenant_id, share_networks=new_quota) self.assertEqual(int(updated["share_networks"]), new_quota) @test.attr(type=["gate", "smoke", ]) def test_update_user_quota_share_networks(self): - client = self.get_client_with_isolated_creds() - # get current quotas - quotas = client.show_quotas( - client.tenant_id, client.user_id) + quotas = self.client.show_quotas( + self.tenant_id, self.user_id) new_quota = int(quotas["share_networks"]) - 1 # set new quota for share-networks - updated = client.update_quotas( - client.tenant_id, client.user_id, + updated = self.client.update_quotas( + self.tenant_id, self.user_id, share_networks=new_quota) self.assertEqual(int(updated["share_networks"]), new_quota) @test.attr(type=["gate", "smoke", ]) def test_reset_tenant_quotas(self): - client = self.get_client_with_isolated_creds() - # get default_quotas - default = client.default_quotas(client.tenant_id) + default = self.client.default_quotas(self.tenant_id) # get current quotas - custom = client.show_quotas(client.tenant_id) + custom = self.client.show_quotas(self.tenant_id) # make quotas for update shares = int(custom["shares"]) + 2 @@ -224,8 +211,8 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest): share_networks = int(custom["share_networks"]) + 2 # set new quota - updated = client.update_quotas( - client.tenant_id, + updated = self.client.update_quotas( + self.tenant_id, shares=shares, snapshots=snapshots, gigabytes=gigabytes, @@ -239,10 +226,10 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest): self.assertEqual(int(updated["share_networks"]), share_networks) # reset customized quotas - client.reset_quotas(client.tenant_id) + self.client.reset_quotas(self.tenant_id) # verify quotas - reseted = client.show_quotas(client.tenant_id) + reseted = self.client.show_quotas(self.tenant_id) self.assertEqual(int(reseted["shares"]), int(default["shares"])) self.assertEqual(int(reseted["snapshots"]), int(default["snapshots"])) self.assertEqual(int(reseted["gigabytes"]), int(default["gigabytes"])) @@ -251,101 +238,86 @@ class SharesAdminQuotasUpdateTest(base.BaseSharesAdminTest): @test.attr(type=["gate", "smoke", ]) def test_unlimited_quota_for_shares(self): - client = self.get_client_with_isolated_creds() - client.update_quotas(client.tenant_id, shares=-1) + self.client.update_quotas(self.tenant_id, shares=-1) - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) self.assertEqual(-1, quotas.get('shares')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_user_quota_for_shares(self): - client = self.get_client_with_isolated_creds() - client.update_quotas( - client.tenant_id, client.user_id, - shares=-1) + self.client.update_quotas( + self.tenant_id, self.user_id, shares=-1) - quotas = client.show_quotas(client.tenant_id, client.user_id) + quotas = self.client.show_quotas(self.tenant_id, self.user_id) self.assertEqual(-1, quotas.get('shares')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_quota_for_snapshots(self): - client = self.get_client_with_isolated_creds() - client.update_quotas(client.tenant_id, snapshots=-1) + self.client.update_quotas(self.tenant_id, snapshots=-1) - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) self.assertEqual(-1, quotas.get('snapshots')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_user_quota_for_snapshots(self): - client = self.get_client_with_isolated_creds() - client.update_quotas( - client.tenant_id, client.user_id, - snapshots=-1) + self.client.update_quotas( + self.tenant_id, self.user_id, snapshots=-1) - quotas = client.show_quotas(client.tenant_id, client.user_id) + quotas = self.client.show_quotas(self.tenant_id, self.user_id) self.assertEqual(-1, quotas.get('snapshots')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_quota_for_gigabytes(self): - client = self.get_client_with_isolated_creds() - client.update_quotas(client.tenant_id, gigabytes=-1) + self.client.update_quotas(self.tenant_id, gigabytes=-1) - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) self.assertEqual(-1, quotas.get('gigabytes')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_quota_for_snapshot_gigabytes(self): - client = self.get_client_with_isolated_creds() - client.update_quotas( - client.tenant_id, snapshot_gigabytes=-1) + self.client.update_quotas( + self.tenant_id, snapshot_gigabytes=-1) - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) self.assertEqual(-1, quotas.get('snapshot_gigabytes')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_user_quota_for_gigabytes(self): - client = self.get_client_with_isolated_creds() - client.update_quotas( - client.tenant_id, client.user_id, - gigabytes=-1) + self.client.update_quotas( + self.tenant_id, self.user_id, gigabytes=-1) - quotas = client.show_quotas(client.tenant_id, client.user_id) + quotas = self.client.show_quotas(self.tenant_id, self.user_id) self.assertEqual(-1, quotas.get('gigabytes')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_user_quota_for_snapshot_gigabytes(self): - client = self.get_client_with_isolated_creds() - client.update_quotas( - client.tenant_id, client.user_id, - snapshot_gigabytes=-1) + self.client.update_quotas( + self.tenant_id, self.user_id, snapshot_gigabytes=-1) - quotas = client.show_quotas(client.tenant_id, client.user_id) + quotas = self.client.show_quotas(self.tenant_id, self.user_id) self.assertEqual(-1, quotas.get('snapshot_gigabytes')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_quota_for_share_networks(self): - client = self.get_client_with_isolated_creds() - client.update_quotas(client.tenant_id, share_networks=-1) + self.client.update_quotas(self.tenant_id, share_networks=-1) - quotas = client.show_quotas(client.tenant_id) + quotas = self.client.show_quotas(self.tenant_id) self.assertEqual(-1, quotas.get('share_networks')) @test.attr(type=["gate", "smoke", ]) def test_unlimited_user_quota_for_share_networks(self): - client = self.get_client_with_isolated_creds() - client.update_quotas( - client.tenant_id, client.user_id, - share_networks=-1) + self.client.update_quotas( + self.tenant_id, self.user_id, share_networks=-1) - quotas = client.show_quotas(client.tenant_id, client.user_id) + quotas = self.client.show_quotas(self.tenant_id, self.user_id) self.assertEqual(-1, quotas.get('share_networks')) diff --git a/manila_tempest_tests/tests/api/admin/test_services.py b/manila_tempest_tests/tests/api/admin/test_services.py index 4b4085ca52..10df866723 100644 --- a/manila_tempest_tests/tests/api/admin/test_services.py +++ b/manila_tempest_tests/tests/api/admin/test_services.py @@ -13,11 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest import test # noqa +import ddt +from tempest import test from manila_tempest_tests.tests.api import base +@ddt.ddt class ServicesAdminTest(base.BaseSharesAdminTest): def setUp(self): @@ -25,60 +27,67 @@ class ServicesAdminTest(base.BaseSharesAdminTest): self.services = self.shares_client.list_services() @test.attr(type=["gate", "smoke", ]) - def test_list_services(self): - services = self.shares_client.list_services() + @ddt.data('shares_client', 'shares_v2_client') + def test_list_services(self, client_name): + services = getattr(self, client_name).list_services() self.assertNotEqual(0, len(services)) for service in services: self.assertIsNotNone(service['id']) @test.attr(type=["gate", "smoke", ]) - def test_get_services_by_host_name(self): + @ddt.data('shares_client', 'shares_v2_client') + def test_get_services_by_host_name(self, client_name): host = self.services[0]["host"] params = {"host": host} - services = self.shares_client.list_services(params) + services = getattr(self, client_name).list_services(params) self.assertNotEqual(0, len(services)) for service in services: self.assertEqual(host, service["host"]) @test.attr(type=["gate", "smoke", ]) - def test_get_services_by_binary_name(self): + @ddt.data('shares_client', 'shares_v2_client') + def test_get_services_by_binary_name(self, client_name): binary = self.services[0]["binary"] params = {"binary": binary, } - services = self.shares_client.list_services(params) + services = getattr(self, client_name).list_services(params) self.assertNotEqual(0, len(services)) for service in services: self.assertEqual(binary, service["binary"]) @test.attr(type=["gate", "smoke", ]) - def test_get_services_by_availability_zone(self): + @ddt.data('shares_client', 'shares_v2_client') + def test_get_services_by_availability_zone(self, client_name): zone = self.services[0]["zone"] params = {"zone": zone, } - services = self.shares_client.list_services(params) + services = getattr(self, client_name).list_services(params) self.assertNotEqual(0, len(services)) for service in services: self.assertEqual(zone, service["zone"]) @test.attr(type=["gate", "smoke", ]) - def test_get_services_by_status(self): + @ddt.data('shares_client', 'shares_v2_client') + def test_get_services_by_status(self, client_name): status = self.services[0]["status"] params = {"status": status, } - services = self.shares_client.list_services(params) + services = getattr(self, client_name).list_services(params) self.assertNotEqual(0, len(services)) for service in services: self.assertEqual(status, service["status"]) @test.attr(type=["gate", "smoke", ]) - def test_get_services_by_state(self): + @ddt.data('shares_client', 'shares_v2_client') + def test_get_services_by_state(self, client_name): state = self.services[0]["state"] params = {"state": state, } - services = self.shares_client.list_services(params) + services = getattr(self, client_name).list_services(params) self.assertNotEqual(0, len(services)) for service in services: self.assertEqual(state, service["state"]) @test.attr(type=["gate", "smoke", ]) - def test_get_services_by_all_filters(self): + @ddt.data('shares_client', 'shares_v2_client') + def test_get_services_by_all_filters(self, client_name): params = { "host": self.services[0]["host"], "binary": self.services[0]["binary"], @@ -86,7 +95,7 @@ class ServicesAdminTest(base.BaseSharesAdminTest): "status": self.services[0]["status"], "state": self.services[0]["state"], } - services = self.shares_client.list_services(params) + services = getattr(self, client_name).list_services(params) self.assertNotEqual(0, len(services)) for service in services: self.assertEqual(params["host"], service["host"]) diff --git a/manila_tempest_tests/tests/api/admin/test_services_negative.py b/manila_tempest_tests/tests/api/admin/test_services_negative.py index 6ed0c05347..07914d3c78 100644 --- a/manila_tempest_tests/tests/api/admin/test_services_negative.py +++ b/manila_tempest_tests/tests/api/admin/test_services_negative.py @@ -13,13 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest import test # noqa -from tempest_lib import exceptions as lib_exc # noqa +import ddt +from tempest import test +from tempest_lib import exceptions as lib_exc from manila_tempest_tests import clients_share as clients from manila_tempest_tests.tests.api import base +@ddt.ddt class ServicesAdminNegativeTest(base.BaseSharesAdminTest): @classmethod @@ -76,3 +78,18 @@ class ServicesAdminNegativeTest(base.BaseSharesAdminTest): params = {'state': 'fake_state'} services_fake = self.shares_client.list_services(params) self.assertEqual(0, len(services_fake)) + + @test.attr(type=["gate", "smoke", "negative", ]) + @ddt.data( + ('os-services', '2.7'), + ('services', '2.6'), + ('services', '2.0'), + ) + @ddt.unpack + @base.skip_if_microversion_not_supported("2.7") + def test_list_services_with_wrong_versions(self, url, version): + self.assertRaises( + lib_exc.NotFound, + self.shares_v2_client.list_services, + version=version, url=url, + ) diff --git a/manila_tempest_tests/tests/api/admin/test_share_manage.py b/manila_tempest_tests/tests/api/admin/test_share_manage.py index bb04dc7cfc..78255ca780 100644 --- a/manila_tempest_tests/tests/api/admin/test_share_manage.py +++ b/manila_tempest_tests/tests/api/admin/test_share_manage.py @@ -79,14 +79,13 @@ class ManageNFSShareTest(base.BaseSharesAdminTest): cls.shares = cls.create_shares([creation_data, creation_data]) # Load all share data (host, etc.) - cls.share1 = cls.shares_client.get_share(cls.shares[0]['id']) - cls.share2 = cls.shares_client.get_share(cls.shares[1]['id']) + cls.share1 = cls.shares_v2_client.get_share(cls.shares[0]['id']) + cls.share2 = cls.shares_v2_client.get_share(cls.shares[1]['id']) # Unmanage shares from manila - cls.shares_client.unmanage_share(cls.share1['id']) - cls.shares_client.wait_for_resource_deletion(share_id=cls.share1['id']) - cls.shares_client.unmanage_share(cls.share2['id']) - cls.shares_client.wait_for_resource_deletion(share_id=cls.share2['id']) + for share_id in (cls.share1['id'], cls.share2['id']): + cls.shares_v2_client.unmanage_share(share_id) + cls.shares_v2_client.wait_for_resource_deletion(share_id=share_id) @test.attr(type=["gate", "smoke"]) def test_manage(self): @@ -94,7 +93,7 @@ class ManageNFSShareTest(base.BaseSharesAdminTest): description = "Description for 'managed' share" # Manage share - share = self.shares_client.manage_share( + share = self.shares_v2_client.manage_share( service_host=self.share1['host'], export_path=self.share1['export_locations'][0], protocol=self.share1['share_proto'], @@ -109,7 +108,7 @@ class ManageNFSShareTest(base.BaseSharesAdminTest): 'client': self.shares_client}) # Wait for success - self.shares_client.wait_for_share_status(share['id'], 'available') + self.shares_v2_client.wait_for_share_status(share['id'], 'available') # Verify data of managed share get = self.shares_v2_client.get_share(share['id'], version="2.5") @@ -123,10 +122,10 @@ class ManageNFSShareTest(base.BaseSharesAdminTest): self.assertEqual(self.st['share_type']['id'], get['share_type']) # Delete share - self.shares_client.delete_share(share['id']) - self.shares_client.wait_for_resource_deletion(share_id=share['id']) + self.shares_v2_client.delete_share(share['id']) + self.shares_v2_client.wait_for_resource_deletion(share_id=share['id']) self.assertRaises(lib_exc.NotFound, - self.shares_client.get_share, + self.shares_v2_client.get_share, share['id']) @test.attr(type=["gate", "smoke"]) @@ -137,7 +136,7 @@ class ManageNFSShareTest(base.BaseSharesAdminTest): (self.st['share_type']['id'], 'available')] for share_type_id, status in parameters: - share = self.shares_client.manage_share( + share = self.shares_v2_client.manage_share( service_host=self.share2['host'], export_path=self.share2['export_locations'][0], protocol=self.share2['share_proto'], @@ -146,16 +145,16 @@ class ManageNFSShareTest(base.BaseSharesAdminTest): # Add managed share to cleanup queue self.method_resources.insert( 0, {'type': 'share', 'id': share['id'], - 'client': self.shares_client}) + 'client': self.shares_v2_client}) # Wait for success - self.shares_client.wait_for_share_status(share['id'], status) + self.shares_v2_client.wait_for_share_status(share['id'], status) # Delete share - self.shares_client.delete_share(share['id']) - self.shares_client.wait_for_resource_deletion(share_id=share['id']) + self.shares_v2_client.delete_share(share['id']) + self.shares_v2_client.wait_for_resource_deletion(share_id=share['id']) self.assertRaises(lib_exc.NotFound, - self.shares_client.get_share, + self.shares_v2_client.get_share, share['id']) diff --git a/manila_tempest_tests/tests/api/admin/test_share_types.py b/manila_tempest_tests/tests/api/admin/test_share_types.py index ceed68b501..d58c61cf9e 100644 --- a/manila_tempest_tests/tests/api/admin/test_share_types.py +++ b/manila_tempest_tests/tests/api/admin/test_share_types.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt from tempest import config # noqa from tempest import test # noqa from tempest_lib.common.utils import data_utils # noqa @@ -24,6 +25,7 @@ from manila_tempest_tests.tests.api import base CONF = config.CONF +@ddt.ddt class ShareTypesAdminTest(base.BaseSharesAdminTest): @test.attr(type=["gate", "smoke", ]) @@ -32,53 +34,76 @@ class ShareTypesAdminTest(base.BaseSharesAdminTest): extra_specs = self.add_required_extra_specs_to_dict() # Create share type - st_create = self.shares_client.create_share_type( + st_create = self.shares_v2_client.create_share_type( name, extra_specs=extra_specs) self.assertEqual(name, st_create['share_type']['name']) st_id = st_create['share_type']['id'] # Delete share type - self.shares_client.delete_share_type(st_id) + self.shares_v2_client.delete_share_type(st_id) # Verify deletion of share type - self.shares_client.wait_for_resource_deletion(st_id=st_id) + self.shares_v2_client.wait_for_resource_deletion(st_id=st_id) self.assertRaises(lib_exc.NotFound, - self.shares_client.get_share_type, + self.shares_v2_client.get_share_type, st_id) + def _verify_is_public_key_name(self, share_type, version): + old_key_name = 'os-share-type-access:is_public' + new_key_name = 'share_type_access:is_public' + if float(version) > 2.6: + self.assertIn(new_key_name, share_type) + self.assertNotIn(old_key_name, share_type) + else: + self.assertIn(old_key_name, share_type) + self.assertNotIn(new_key_name, share_type) + @test.attr(type=["gate", "smoke", ]) - def test_share_type_create_get(self): + @ddt.data('2.0', '2.6', '2.7') + def test_share_type_create_get(self, version): + self.skip_if_microversion_not_supported(version) + name = data_utils.rand_name("tempest-manila") extra_specs = self.add_required_extra_specs_to_dict({"key": "value", }) # Create share type - st_create = self.create_share_type(name, extra_specs=extra_specs) + st_create = self.create_share_type( + name, extra_specs=extra_specs, version=version) self.assertEqual(name, st_create['share_type']['name']) + self._verify_is_public_key_name(st_create['share_type'], version) st_id = st_create["share_type"]["id"] # Get share type - get = self.shares_client.get_share_type(st_id) + get = self.shares_v2_client.get_share_type(st_id, version=version) self.assertEqual(name, get["share_type"]["name"]) self.assertEqual(st_id, get["share_type"]["id"]) self.assertEqual(extra_specs, get["share_type"]["extra_specs"]) + self._verify_is_public_key_name(get['share_type'], version) # Check that backwards compatibility didn't break self.assertDictMatch(get["volume_type"], get["share_type"]) @test.attr(type=["gate", "smoke", ]) - def test_share_type_create_list(self): + @ddt.data('2.0', '2.6', '2.7') + def test_share_type_create_list(self, version): + self.skip_if_microversion_not_supported(version) + name = data_utils.rand_name("tempest-manila") extra_specs = self.add_required_extra_specs_to_dict() # Create share type - st_create = self.create_share_type(name, extra_specs=extra_specs) + st_create = self.create_share_type( + name, extra_specs=extra_specs, version=version) + self._verify_is_public_key_name(st_create['share_type'], version) st_id = st_create["share_type"]["id"] # list share types - st_list = self.shares_client.list_share_types() + st_list = self.shares_v2_client.list_share_types(version=version) sts = st_list["share_types"] self.assertTrue(len(sts) >= 1) self.assertTrue(any(st_id in st["id"] for st in sts)) + for st in sts: + self._verify_is_public_key_name(st, version) # Check that backwards compatibility didn't break vts = st_list["volume_types"] @@ -128,16 +153,16 @@ class ShareTypesAdminTest(base.BaseSharesAdminTest): st_id = st_create["share_type"]["id"] # It should not be listed without access - st_list = self.shares_client.list_share_types() + st_list = self.shares_v2_client.list_share_types() sts = st_list["share_types"] self.assertFalse(any(st_id in st["id"] for st in sts)) # List projects that have access for share type - none expected - access = self.shares_client.list_access_to_share_type(st_id) + access = self.shares_v2_client.list_access_to_share_type(st_id) self.assertEqual([], access) # Add project access to share type - access = self.shares_client.add_access_to_share_type( + access = self.shares_v2_client.add_access_to_share_type( st_id, project_id) # Now it should be listed @@ -146,12 +171,12 @@ class ShareTypesAdminTest(base.BaseSharesAdminTest): self.assertTrue(any(st_id in st["id"] for st in sts)) # List projects that have access for share type - one expected - access = self.shares_client.list_access_to_share_type(st_id) + access = self.shares_v2_client.list_access_to_share_type(st_id) expected = [{'share_type_id': st_id, 'project_id': project_id}, ] self.assertEqual(expected, access) # Remove project access from share type - access = self.shares_client.remove_access_from_share_type( + access = self.shares_v2_client.remove_access_from_share_type( st_id, project_id) # It should not be listed without access @@ -160,5 +185,5 @@ class ShareTypesAdminTest(base.BaseSharesAdminTest): self.assertFalse(any(st_id in st["id"] for st in sts)) # List projects that have access for share type - none expected - access = self.shares_client.list_access_to_share_type(st_id) + access = self.shares_v2_client.list_access_to_share_type(st_id) self.assertEqual([], access) diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py index b2da08047c..de7c542ffa 100644 --- a/manila_tempest_tests/tests/api/base.py +++ b/manila_tempest_tests/tests/api/base.py @@ -26,6 +26,7 @@ from tempest import config from tempest import test from tempest_lib.common.utils import data_utils from tempest_lib import exceptions +import testtools from manila_tempest_tests import clients_share as clients from manila_tempest_tests import share_exceptions @@ -89,6 +90,21 @@ def network_synchronized(f): return wrapped_func +def is_microversion_supported(microversion): + if (float(microversion) > float(CONF.share.max_api_microversion) or + float(microversion) < float(CONF.share.min_api_microversion)): + return False + return True + + +def skip_if_microversion_not_supported(microversion): + """Decorator for tests that are microversion-specific.""" + if not is_microversion_supported(microversion): + reason = ("Skipped. Test requires microversion '%s'." % microversion) + return testtools.skip(reason) + return lambda f: f + + class BaseSharesTest(test.BaseTestCase): """Base test case class for all Manila API tests.""" @@ -107,6 +123,11 @@ class BaseSharesTest(test.BaseTestCase): # Will be cleaned up in tearDown method method_isolated_creds = [] + def skip_if_microversion_not_supported(self, microversion): + if not is_microversion_supported(microversion): + raise self.skipException( + "Microversion '%s' is not supported." % microversion) + @classmethod def get_client_with_isolated_creds(cls, name=None, @@ -525,7 +546,7 @@ class BaseSharesTest(test.BaseTestCase): def create_share_type(cls, name, is_public=True, client=None, cleanup_in_class=True, **kwargs): if client is None: - client = cls.shares_client + client = cls.shares_v2_client share_type = client.create_share_type(name, is_public, **kwargs) resource = { "type": "share_type", diff --git a/manila_tempest_tests/tests/api/test_availability_zones.py b/manila_tempest_tests/tests/api/test_availability_zones.py index 9176683257..8230157ed1 100644 --- a/manila_tempest_tests/tests/api/test_availability_zones.py +++ b/manila_tempest_tests/tests/api/test_availability_zones.py @@ -29,8 +29,23 @@ class AvailabilityZonesTest(base.BaseSharesTest): self.assertIn(key, az) @test.attr(type=["smoke", "gate"]) - def test_list_availability_zones_extension_url(self): + def test_list_availability_zones_legacy_url_api_v1(self): # NOTE(vponomaryov): remove this test with removal of availability zone # extension url support. azs = self.shares_client.list_availability_zones() self._list_availability_zones_assertions(azs) + + @test.attr(type=["smoke", "gate"]) + @base.skip_if_microversion_not_supported("2.6") + def test_list_availability_zones_legacy_url_api_v2(self): + # NOTE(vponomaryov): remove this test with removal of availability zone + # extension url support. + azs = self.shares_v2_client.list_availability_zones( + url='os-availability-zone', version='2.6') + self._list_availability_zones_assertions(azs) + + @test.attr(type=["smoke", "gate"]) + @base.skip_if_microversion_not_supported("2.7") + def test_list_availability_zones(self): + azs = self.shares_v2_client.list_availability_zones(version='2.7') + self._list_availability_zones_assertions(azs) diff --git a/manila_tempest_tests/tests/api/test_availability_zones_negative.py b/manila_tempest_tests/tests/api/test_availability_zones_negative.py new file mode 100644 index 0000000000..0a562c69d5 --- /dev/null +++ b/manila_tempest_tests/tests/api/test_availability_zones_negative.py @@ -0,0 +1,43 @@ +# Copyright 2015 Mirantis Inc. +# 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. + +from tempest import test +from tempest_lib import exceptions as lib_exc + +from manila_tempest_tests.tests.api import base + + +@base.skip_if_microversion_not_supported("2.7") +class AvailabilityZonesNegativeTest(base.BaseSharesTest): + + @test.attr(type=["smoke", "gate"]) + def test_list_availability_zones_api_not_found_with_legacy_url(self): + # NOTE(vponomaryov): remove this test with removal of availability zone + # extension url support. + self.assertRaises( + lib_exc.NotFound, + self.shares_v2_client.list_availability_zones, + url='os-availability-zone', + version='2.7', + ) + + @test.attr(type=["smoke", "gate"]) + def test_list_availability_zones_api_not_found(self): + self.assertRaises( + lib_exc.NotFound, + self.shares_v2_client.list_availability_zones, + url='availability-zones', + version='2.6', + ) diff --git a/manila_tempest_tests/tests/api/test_quotas.py b/manila_tempest_tests/tests/api/test_quotas.py index af5885af71..da6dcce554 100644 --- a/manila_tempest_tests/tests/api/test_quotas.py +++ b/manila_tempest_tests/tests/api/test_quotas.py @@ -13,22 +13,25 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt from tempest import test # noqa from manila_tempest_tests.tests.api import base +@ddt.data class SharesQuotasTest(base.BaseSharesTest): @classmethod def resource_setup(cls): super(SharesQuotasTest, cls).resource_setup() - cls.user_id = cls.shares_client.user_id - cls.tenant_id = cls.shares_client.tenant_id + cls.user_id = cls.shares_v2_client.user_id + cls.tenant_id = cls.shares_v2_client.tenant_id @test.attr(type=["gate", "smoke", ]) - def test_default_quotas(self): - quotas = self.shares_client.default_quotas(self.tenant_id) + @ddt.data('shares_client', 'shares_v2_client') + def test_default_quotas(self, client_name): + quotas = getattr(self, client_name).default_quotas(self.tenant_id) self.assertGreater(int(quotas["gigabytes"]), -2) self.assertGreater(int(quotas["snapshot_gigabytes"]), -2) self.assertGreater(int(quotas["shares"]), -2) @@ -36,8 +39,9 @@ class SharesQuotasTest(base.BaseSharesTest): self.assertGreater(int(quotas["share_networks"]), -2) @test.attr(type=["gate", "smoke", ]) - def test_show_quotas(self): - quotas = self.shares_client.show_quotas(self.tenant_id) + @ddt.data('shares_client', 'shares_v2_client') + def test_show_quotas(self, client_name): + quotas = getattr(self, client_name).show_quotas(self.tenant_id) self.assertGreater(int(quotas["gigabytes"]), -2) self.assertGreater(int(quotas["snapshot_gigabytes"]), -2) self.assertGreater(int(quotas["shares"]), -2) @@ -45,8 +49,9 @@ class SharesQuotasTest(base.BaseSharesTest): self.assertGreater(int(quotas["share_networks"]), -2) @test.attr(type=["gate", "smoke", ]) - def test_show_quotas_for_user(self): - quotas = self.shares_client.show_quotas( + @ddt.data('shares_client', 'shares_v2_client') + def test_show_quotas_for_user(self, client_name): + quotas = getattr(self, client_name).show_quotas( self.tenant_id, self.user_id) self.assertGreater(int(quotas["gigabytes"]), -2) self.assertGreater(int(quotas["snapshot_gigabytes"]), -2) diff --git a/manila_tempest_tests/tests/api/test_quotas_negative.py b/manila_tempest_tests/tests/api/test_quotas_negative.py index 0736b98ae7..e2d2c37691 100644 --- a/manila_tempest_tests/tests/api/test_quotas_negative.py +++ b/manila_tempest_tests/tests/api/test_quotas_negative.py @@ -13,29 +13,55 @@ # License for the specific language governing permissions and limitations # under the License. -from tempest import test # noqa -from tempest_lib import exceptions as lib_exc # noqa -import testtools # noqa +import ddt +from tempest import test +from tempest_lib import exceptions as lib_exc from manila_tempest_tests.tests.api import base +@ddt.ddt class SharesQuotasNegativeTest(base.BaseSharesTest): @test.attr(type=["gate", "smoke", "negative"]) def test_get_quotas_with_empty_tenant_id(self): self.assertRaises(lib_exc.NotFound, - self.shares_client.show_quotas, "") + self.shares_v2_client.show_quotas, "") @test.attr(type=["gate", "smoke", "negative", ]) def test_try_reset_quotas_with_user(self): self.assertRaises(lib_exc.Forbidden, - self.shares_client.reset_quotas, - self.shares_client.tenant_id) + self.shares_v2_client.reset_quotas, + self.shares_v2_client.tenant_id) @test.attr(type=["gate", "smoke", "negative", ]) def test_try_update_quotas_with_user(self): self.assertRaises(lib_exc.Forbidden, - self.shares_client.update_quotas, - self.shares_client.tenant_id, + self.shares_v2_client.update_quotas, + self.shares_v2_client.tenant_id, shares=9) + + @ddt.data( + ('services', '2.0', 'show_quotas'), + ('services', '2.0', 'default_quotas'), + ('services', '2.0', 'reset_quotas'), + ('services', '2.0', 'update_quotas'), + ('services', '2.6', 'show_quotas'), + ('services', '2.6', 'default_quotas'), + ('services', '2.6', 'reset_quotas'), + ('services', '2.6', 'update_quotas'), + ('os-services', '2.7', 'show_quotas'), + ('os-services', '2.7', 'default_quotas'), + ('os-services', '2.7', 'reset_quotas'), + ('os-services', '2.7', 'update_quotas'), + ) + @ddt.unpack + @test.attr(type=["gate", "smoke", "negative", ]) + @base.skip_if_microversion_not_supported("2.7") + def test_show_quotas_with_wrong_versions(self, url, version, method_name): + self.assertRaises( + lib_exc.NotFound, + getattr(self.shares_v2_client, method_name), + self.shares_v2_client.tenant_id, + version=version, url=url, + )