From dddc0688796c764888f98a3e3e6ba7ea3cbf1c98 Mon Sep 17 00:00:00 2001 From: Clinton Knight Date: Thu, 27 Aug 2015 15:00:23 -0400 Subject: [PATCH] Add v2 Manila API path as base for microversions To prevent a microversioned client from managing a non-microversioned Manila server, Manila must update its REST endpoints by adding /v2 for all microversioned APIs. This commit does the following: * Add /v2 to the URL map, connected to all the same /v1 API methods * Renumber the microversion sequence starting from 2.0 * Update the versions API to reflect v2 * Publish the new endpoint to Keystone in the DevStack plug-in * Update relevant documentation * Update Tempest tests for microversions APIImpact Co-Authored-By: Andrew Kerr Closes-Bug: 1488624 Change-Id: I56a516b5f81914557dd2465746629431cfd6deac --- devstack/plugin.sh | 8 + doc/source/adminref/quick_start.rst | 33 ++ etc/manila/api-paste.ini | 13 +- manila/api/contrib/admin_actions.py | 8 +- manila/api/openstack/api_version_request.py | 16 +- .../openstack/rest_api_version_history.rst | 30 +- manila/api/openstack/wsgi.py | 56 +- manila/api/v1/cgsnapshots.py | 14 +- manila/api/v1/consistency_groups.py | 12 +- manila/api/v1/share_instances.py | 6 +- manila/api/v1/shares.py | 6 +- manila/api/versions.py | 59 +- manila/api/views/shares.py | 2 +- manila/api/views/versions.py | 7 +- .../tests/api/contrib/test_admin_actions.py | 16 +- manila/tests/api/test_versions.py | 190 ++++--- manila/tests/api/v1/test_cgsnapshots.py | 2 +- .../tests/api/v1/test_consistency_groups.py | 2 +- manila/tests/api/v1/test_share_instances.py | 2 +- manila/tests/api/v1/test_shares.py | 16 +- manila_tempest_tests/clients_share.py | 4 + manila_tempest_tests/config.py | 4 +- .../services/share/json/shares_client.py | 340 +----------- .../services/share/v2/__init__.py | 0 .../services/share/v2/json/__init__.py | 0 .../services/share/v2/json/shares_client.py | 516 ++++++++++++++++++ .../tests/api/admin/test_admin_actions.py | 10 +- .../api/admin/test_admin_actions_negative.py | 9 +- .../admin/test_consistency_group_actions.py | 38 +- .../api/admin/test_consistency_groups.py | 8 +- .../admin/test_consistency_groups_negative.py | 189 ++++--- .../tests/api/admin/test_migration.py | 4 +- .../tests/api/admin/test_share_instances.py | 78 +++ manila_tempest_tests/tests/api/base.py | 42 +- .../api/test_consistency_group_actions.py | 101 ++-- .../tests/api/test_consistency_groups.py | 45 +- .../api/test_consistency_groups_negative.py | 150 ++--- .../tests/api/test_microversions.py | 183 +++++-- 38 files changed, 1383 insertions(+), 836 deletions(-) create mode 100644 manila_tempest_tests/services/share/v2/__init__.py create mode 100644 manila_tempest_tests/services/share/v2/json/__init__.py create mode 100644 manila_tempest_tests/services/share/v2/json/shares_client.py create mode 100644 manila_tempest_tests/tests/api/admin/test_share_instances.py diff --git a/devstack/plugin.sh b/devstack/plugin.sh index a948c282..2485f621 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -434,11 +434,19 @@ function create_manila_accounts { create_service_user "manila" if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then + # Set up Manila v1 service and endpoint get_or_create_service "manila" "share" "Manila Shared Filesystem Service" get_or_create_endpoint "share" "$REGION_NAME" \ "$MANILA_SERVICE_PROTOCOL://$MANILA_SERVICE_HOST:$MANILA_SERVICE_PORT/v1/\$(tenant_id)s" \ "$MANILA_SERVICE_PROTOCOL://$MANILA_SERVICE_HOST:$MANILA_SERVICE_PORT/v1/\$(tenant_id)s" \ "$MANILA_SERVICE_PROTOCOL://$MANILA_SERVICE_HOST:$MANILA_SERVICE_PORT/v1/\$(tenant_id)s" + + # Set up Manila v2 service and endpoint + get_or_create_service "manilav2" "sharev2" "Manila Shared Filesystem Service V2" + get_or_create_endpoint "sharev2" "$REGION_NAME" \ + "$MANILA_SERVICE_PROTOCOL://$MANILA_SERVICE_HOST:$MANILA_SERVICE_PORT/v2/\$(tenant_id)s" \ + "$MANILA_SERVICE_PROTOCOL://$MANILA_SERVICE_HOST:$MANILA_SERVICE_PORT/v2/\$(tenant_id)s" \ + "$MANILA_SERVICE_PROTOCOL://$MANILA_SERVICE_HOST:$MANILA_SERVICE_PORT/v2/\$(tenant_id)s" fi } diff --git a/doc/source/adminref/quick_start.rst b/doc/source/adminref/quick_start.rst index 730f0f66..147deaa8 100644 --- a/doc/source/adminref/quick_start.rst +++ b/doc/source/adminref/quick_start.rst @@ -132,6 +132,11 @@ Here are the registration steps, similar to those of Cinder: --type share \ --description "OpenStack Shared Filesystems" + $ keystone service-create \ + --name manilav2 \ + --type sharev2 \ + --description "OpenStack Shared Filesystems V2" + Result:: @@ -145,6 +150,16 @@ Result:: | type | share | +-------------+----------------------------------+ + +-------------+----------------------------------+ + | Property | Value | + +-------------+----------------------------------+ + | description | OpenStack Shared Filesystems V2 | + | enabled | True | + | id | 2840d1e7b033437f8776a7bd5045b28d | + | name | manilav2 | + | type | sharev2 | + +-------------+----------------------------------+ + 4) Create the Share Filesystems service API endpoints:: @@ -155,6 +170,13 @@ Result:: --adminurl http://controller:8786/v1/%\(tenant_id\)s \ --region regionOne + $ keystone endpoint-create \ + --service-id $(keystone service-list | awk '/ sharev2 / {print $2}') \ + --publicurl http://controller:8786/v2/%\(tenant_id\)s \ + --internalurl http://controller:8786/v2/%\(tenant_id\)s \ + --adminurl http://controller:8786/v2/%\(tenant_id\)s \ + --region regionOne + Result:: +-------------+-----------------------------------------+ @@ -168,6 +190,17 @@ Result:: | service_id | 4c13e9ff7ec04f4e95a26f72ecdf9919 | +-------------+-----------------------------------------+ + +-------------+-----------------------------------------+ + | Property | Value | + +-------------+-----------------------------------------+ + | adminurl | http://controller:8786/v2/%(tenant_id)s | + | id | 63ddffd27e8c4c62b4ffb228083325e6 | + | internalurl | http://controller:8786/v2/%(tenant_id)s | + | publicurl | http://controller:8786/v2/%(tenant_id)s | + | region | regionOne | + | service_id | 2840d1e7b033437f8776a7bd5045b28d | + +-------------+-----------------------------------------+ + .. note:: Port ‘8786’ is the default port for Manila. It may be changed to any other port, but this change should also be made in the Manila configuration diff --git a/etc/manila/api-paste.ini b/etc/manila/api-paste.ini index a0ab3597..f3f07e4d 100644 --- a/etc/manila/api-paste.ini +++ b/etc/manila/api-paste.ini @@ -5,13 +5,14 @@ [composite:osapi_share] use = call:manila.api:root_app_factory /: apiversions -/v1: openstack_share_api_v1 +/v1: openstack_share_api +/v2: openstack_share_api -[composite:openstack_share_api_v1] +[composite:openstack_share_api] use = call:manila.api.middleware.auth:pipeline_factory -noauth = faultwrap ssl sizelimit noauth apiv1 -keystone = faultwrap ssl sizelimit authtoken keystonecontext apiv1 -keystone_nolimit = faultwrap ssl sizelimit authtoken keystonecontext apiv1 +noauth = faultwrap ssl sizelimit noauth api +keystone = faultwrap ssl sizelimit authtoken keystonecontext api +keystone_nolimit = faultwrap ssl sizelimit authtoken keystonecontext api [filter:faultwrap] paste.filter_factory = manila.api.middleware.fault:FaultWrapper.factory @@ -25,7 +26,7 @@ paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory [filter:ssl] paste.filter_factory = oslo_middleware.ssl:SSLMiddleware.factory -[app:apiv1] +[app:api] paste.app_factory = manila.api.v1.router:APIRouter.factory [pipeline:apiversions] diff --git a/manila/api/contrib/admin_actions.py b/manila/api/contrib/admin_actions.py index 2c393395..a6fb9f05 100644 --- a/manila/api/contrib/admin_actions.py +++ b/manila/api/contrib/admin_actions.py @@ -167,15 +167,15 @@ class CGAdminController(AdminController): def _delete(self, context, resource, force=True): db.consistency_group_destroy(context.elevated(), resource['id']) + @wsgi.Controller.api_version('2.4', experimental=True) @wsgi.action('os-reset_status') @wsgi.response(202) - @wsgi.Controller.api_version('1.5', experimental=True) def cg_reset_status(self, req, id, body): super(CGAdminController, self)._reset_status(req, id, body) + @wsgi.Controller.api_version('2.4', experimental=True) @wsgi.action('os-force_delete') @wsgi.response(202) - @wsgi.Controller.api_version('1.5', experimental=True) def cg_force_delete(self, req, id, body): super(CGAdminController, self)._force_delete(req, id, body) @@ -198,15 +198,15 @@ class CGSnapshotAdminController(AdminController): def _delete(self, context, resource, force=True): db.cgsnapshot_destroy(context.elevated(), resource['id']) + @wsgi.Controller.api_version('2.4', experimental=True) @wsgi.action('os-reset_status') @wsgi.response(202) - @wsgi.Controller.api_version('1.5', experimental=True) def cgsnapshot_reset_status(self, req, id, body): super(CGSnapshotAdminController, self)._reset_status(req, id, body) + @wsgi.Controller.api_version('2.4', experimental=True) @wsgi.action('os-force_delete') @wsgi.response(202) - @wsgi.Controller.api_version('1.5', experimental=True) def cgsnapshot_force_delete(self, req, id, body): super(CGSnapshotAdminController, self)._force_delete(req, id, body) diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index 947706c6..b5cbada6 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -45,20 +45,20 @@ REST_API_VERSION_HISTORY = """ REST API Version History: * 1.0 - Initial version. Includes all V1 APIs and extensions in Kilo. - * 1.1 - Versions API updated to reflect beginning of microversions epoch. - * 1.2 - Share create() doesn't ignore availability_zone field of share. - * 1.3 - Snapshots become optional feature. - * 1.4 - Share instances admin API - * 1.5 - Consistency Group support - * 1.6 - Share Migration admin API + * 2.0 - Versions API updated to reflect beginning of microversions epoch. + * 2.1 - Share create() doesn't ignore availability_zone field of share. + * 2.2 - Snapshots become optional feature. + * 2.3 - Share instances admin API + * 2.4 - Consistency Group support + * 2.5 - Share Migration admin API """ # The minimum and maximum versions of the API supported # The default api version request is defined to be the # the minimum version of the API supported. -_MIN_API_VERSION = "1.0" -_MAX_API_VERSION = "1.6" +_MIN_API_VERSION = "2.0" +_MAX_API_VERSION = "2.5" 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 9ca7eab4..cbc73a89 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -8,15 +8,15 @@ user documentation. 1.0 --- - The 1.0 Manila API includes all v1 core APIs existing prior to - the introduction of microversions. + the introduction of microversions. The /v1 URL is used to call + 1.0 APIs, and microversions headers sent to this endpoint are + ignored. -1.1 +2.0 --- - This is the initial version of the Manila API which supports - microversions. + microversions. The /v2 URL is used to call 2.x APIs. A user can specify a header in the API request:: @@ -24,33 +24,35 @@ user documentation. where ```` is any valid api version for this API. - If no version is specified then the API will behave as version 1.0 + If no version is specified then the API will behave as if version 2.0 was requested. - The only API change in version 1.1 is versions, i.e. - GET http://localhost:8786/, which now returns the minimum and - current microversion values. + The only API change in version 2.0 is versions, i.e. + GET http://localhost:8786/, which now returns information about + both 1.0 and 2.x versions and their respective /v1 and /v2 endpoints. -1.2 + All other 2.0 APIs are functionally identical to version 1.0. + +2.1 --- Share create() method doesn't ignore availability_zone field of provided share. -1.3 +2.2 --- Snapshots become optional and share payload now has boolean attr 'snapshot_support'. -1.4 +2.3 --- Share instances admin API and update of Admin Actions extension. -1.5 +2.4 --- Consistency groups support. /consistency-groups and /cgsnapshots are implemented. AdminActions 'os-force_delete and' 'os-reset_status' have been updated for both new resources. -1.6 +2.5 --- Share Migration admin API. diff --git a/manila/api/openstack/wsgi.py b/manila/api/openstack/wsgi.py index cc5401db..f8a741a4 100644 --- a/manila/api/openstack/wsgi.py +++ b/manila/api/openstack/wsgi.py @@ -51,6 +51,8 @@ VER_METHOD_ATTR = 'versioned_methods' API_VERSION_REQUEST_HEADER = 'X-OpenStack-Manila-API-Version' EXPERIMENTAL_API_REQUEST_HEADER = 'X-OpenStack-Manila-API-Experimental' +V1_SCRIPT_NAME = '/v1' + class Request(webob.Request): """Add some OpenStack API-specific logic to the base webob.Request.""" @@ -207,30 +209,40 @@ class Request(webob.Request): return content_type def set_api_version_request(self): - """Set API version request based on the request header information.""" - if API_VERSION_REQUEST_HEADER in self.headers: - hdr_string = self.headers[API_VERSION_REQUEST_HEADER] - # 'latest' is a special keyword which is equivalent to requesting - # the maximum version of the API supported - if hdr_string == 'latest': - self.api_version_request = api_version.max_api_version() + """Set API version request based on the request header information. + + Microversions starts with /v2, so if a client sends a /v1 URL, then + ignore the headers and request 1.0 APIs. + """ + + if not self.script_name: + self.api_version_request = api_version.APIVersionRequest() + elif self.script_name == V1_SCRIPT_NAME: + self.api_version_request = api_version.APIVersionRequest('1.0') + else: + if API_VERSION_REQUEST_HEADER in self.headers: + hdr_string = self.headers[API_VERSION_REQUEST_HEADER] + # 'latest' is a special keyword which is equivalent to + # requesting the maximum version of the API supported + if hdr_string == 'latest': + self.api_version_request = api_version.max_api_version() + else: + self.api_version_request = api_version.APIVersionRequest( + hdr_string) + + # Check that the version requested is within the global + # minimum/maximum of supported API versions + if not self.api_version_request.matches( + api_version.min_api_version(), + api_version.max_api_version()): + raise exception.InvalidGlobalAPIVersion( + req_ver=self.api_version_request.get_string(), + min_ver=api_version.min_api_version().get_string(), + max_ver=api_version.max_api_version().get_string()) + else: self.api_version_request = api_version.APIVersionRequest( - hdr_string) - - # Check that the version requested is within the global - # minimum/maximum of supported API versions - if not self.api_version_request.matches( - api_version.min_api_version(), - api_version.max_api_version()): - raise exception.InvalidGlobalAPIVersion( - req_ver=self.api_version_request.get_string(), - min_ver=api_version.min_api_version().get_string(), - max_ver=api_version.max_api_version().get_string()) - - else: - self.api_version_request = api_version.APIVersionRequest( - api_version.DEFAULT_API_VERSION) + api_version.DEFAULT_API_VERSION) # Check if experimental API was requested if EXPERIMENTAL_API_REQUEST_HEADER in self.headers: diff --git a/manila/api/v1/cgsnapshots.py b/manila/api/v1/cgsnapshots.py index ff4f0910..1a9b6f46 100644 --- a/manila/api/v1/cgsnapshots.py +++ b/manila/api/v1/cgsnapshots.py @@ -41,7 +41,7 @@ class CGSnapshotController(wsgi.Controller): super(CGSnapshotController, self).__init__() self.cg_api = cg_api.API() - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def show(self, req, id): """Return data about the given cgsnapshot.""" context = req.environ['manila.context'] @@ -54,7 +54,7 @@ class CGSnapshotController(wsgi.Controller): return self._view_builder.detail(req, cg) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def delete(self, req, id): """Delete a cgsnapshot.""" context = req.environ['manila.context'] @@ -75,12 +75,12 @@ class CGSnapshotController(wsgi.Controller): return webob.Response(status_int=202) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def index(self, req): """Returns a summary list of cgsnapshots.""" return self._get_cgs(req, is_detail=False) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def detail(self, req): """Returns a detailed list of cgsnapshots.""" return self._get_cgs(req, is_detail=True) @@ -107,7 +107,7 @@ class CGSnapshotController(wsgi.Controller): snaps = self._view_builder.summary_list(req, limited_list) return snaps - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def update(self, req, id, body): """Update a cgsnapshot.""" context = req.environ['manila.context'] @@ -135,7 +135,7 @@ class CGSnapshotController(wsgi.Controller): cg = self.cg_api.update_cgsnapshot(context, cg, cg_data) return self._view_builder.detail(req, cg) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) @wsgi.response(202) def create(self, req, body): """Creates a new cgsnapshot.""" @@ -174,7 +174,7 @@ class CGSnapshotController(wsgi.Controller): return self._view_builder.detail(req, dict(six.iteritems( new_snapshot))) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def members(self, req, id): """Returns a list of cgsnapshot members.""" context = req.environ['manila.context'] diff --git a/manila/api/v1/consistency_groups.py b/manila/api/v1/consistency_groups.py index 13d20c38..1ff86df1 100644 --- a/manila/api/v1/consistency_groups.py +++ b/manila/api/v1/consistency_groups.py @@ -42,7 +42,7 @@ class CGController(wsgi.Controller): super(CGController, self).__init__() self.cg_api = cg_api.API() - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def show(self, req, id): """Return data about the given CG.""" context = req.environ['manila.context'] @@ -55,7 +55,7 @@ class CGController(wsgi.Controller): return self._view_builder.detail(req, cg) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def delete(self, req, id): """Delete a CG.""" context = req.environ['manila.context'] @@ -76,12 +76,12 @@ class CGController(wsgi.Controller): return webob.Response(status_int=202) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def index(self, req): """Returns a summary list of shares.""" return self._get_cgs(req, is_detail=False) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def detail(self, req): """Returns a detailed list of shares.""" return self._get_cgs(req, is_detail=True) @@ -108,7 +108,7 @@ class CGController(wsgi.Controller): cgs = self._view_builder.summary_list(req, limited_list) return cgs - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) def update(self, req, id, body): """Update a share.""" context = req.environ['manila.context'] @@ -136,7 +136,7 @@ class CGController(wsgi.Controller): cg = self.cg_api.update(context, cg, cg_data) return self._view_builder.detail(req, cg) - @wsgi.Controller.api_version('1.5', experimental=True) + @wsgi.Controller.api_version('2.4', experimental=True) @wsgi.response(202) def create(self, req, body): """Creates a new share.""" diff --git a/manila/api/v1/share_instances.py b/manila/api/v1/share_instances.py index 624f65d4..98704fd8 100644 --- a/manila/api/v1/share_instances.py +++ b/manila/api/v1/share_instances.py @@ -39,7 +39,7 @@ class ShareInstancesController(wsgi.Controller): except exception.PolicyNotAuthorized: raise exc.HTTPForbidden() - @wsgi.Controller.api_version("1.4") + @wsgi.Controller.api_version("2.3") def index(self, req): context = req.environ['manila.context'] self._authorize(context, 'index') @@ -47,7 +47,7 @@ class ShareInstancesController(wsgi.Controller): instances = db.share_instances_get_all(context) return self._view_builder.detail_list(req, instances) - @wsgi.Controller.api_version("1.4") + @wsgi.Controller.api_version("2.3") def show(self, req, id): context = req.environ['manila.context'] self._authorize(context, 'show') @@ -59,7 +59,7 @@ class ShareInstancesController(wsgi.Controller): return self._view_builder.detail(req, instance) - @wsgi.Controller.api_version("1.4") + @wsgi.Controller.api_version("2.3") def get_share_instances(self, req, share_id): context = req.environ['manila.context'] self._authorize(context, 'index') diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index abd2fbab..54cab88b 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -89,7 +89,7 @@ class ShareController(wsgi.Controller): return webob.Response(status_int=202) - @wsgi.Controller.api_version("1.6", None, True) + @wsgi.Controller.api_version("2.5", None, True) @wsgi.action("os-migrate_share") def migrate_share(self, req, id, body): """Migrate a share to the specified host.""" @@ -205,11 +205,11 @@ class ShareController(wsgi.Controller): share.update(update_dict) return self._view_builder.detail(req, share) - @wsgi.Controller.api_version("1.5") + @wsgi.Controller.api_version("2.4") def create(self, req, body): return self._create(req, body) - @wsgi.Controller.api_version("1.0", "1.4") # noqa + @wsgi.Controller.api_version("1.0", "2.3") # noqa def create(self, req, body): # pylint: disable=E0102 # Remove consistency group attributes share = body.get('share', {}) diff --git a/manila/api/versions.py b/manila/api/versions.py index eeea8560..3bfdb726 100644 --- a/manila/api/versions.py +++ b/manila/api/versions.py @@ -26,27 +26,35 @@ from manila.api.views import versions as views_versions CONF = cfg.CONF +_LINKS = [{ + 'rel': 'describedby', + 'type': 'text/html', + 'href': 'http://docs.openstack.org/', +}] + +_MEDIA_TYPES = [{ + 'base': 'application/json', + 'type': 'application/vnd.openstack.share+json;version=1', +}] _KNOWN_VERSIONS = { 'v1.0': { 'id': 'v1.0', + 'status': 'SUPPORTED', + 'version': '', + 'min_version': '', + 'updated': '2015-08-27T11:33:21Z', + 'links': _LINKS, + 'media-types': _MEDIA_TYPES, + }, + 'v2.0': { + 'id': 'v2.0', 'status': 'CURRENT', 'version': api_version_request._MAX_API_VERSION, 'min_version': api_version_request._MIN_API_VERSION, - 'updated': '2015-07-30T11:33:21Z', - 'links': [ - { - 'rel': 'describedby', - 'type': 'text/html', - 'href': 'http://docs.openstack.org/', - }, - ], - 'media-types': [ - { - 'base': 'application/json', - 'type': 'application/vnd.openstack.share+json;version=1', - } - ], + 'updated': '2015-08-27T11:33:21Z', + 'links': _LINKS, + 'media-types': _MEDIA_TYPES, }, } @@ -60,7 +68,7 @@ class VersionsRouter(openstack.APIRouter): self.resources['versions'] = create_resource() mapper.connect('versions', '/', controller=self.resources['versions'], - action='index') + action='all') mapper.redirect('', '/') @@ -71,16 +79,27 @@ class VersionsController(wsgi.Controller): @wsgi.Controller.api_version('1.0', '1.0') def index(self, req): - """Return all versions.""" + """Return versions supported prior to the microversions epoch.""" builder = views_versions.get_view_builder(req) known_versions = copy.deepcopy(_KNOWN_VERSIONS) - known_versions['v1.0'].pop('min_version') - known_versions['v1.0'].pop('version') + known_versions.pop('v2.0') return builder.build_versions(known_versions) - @wsgi.Controller.api_version('1.1') # noqa + @wsgi.Controller.api_version('2.0') # noqa def index(self, req): # pylint: disable=E0102 - """Return all versions.""" + """Return versions supported after the start of microversions.""" + builder = views_versions.get_view_builder(req) + known_versions = copy.deepcopy(_KNOWN_VERSIONS) + known_versions.pop('v1.0') + return builder.build_versions(known_versions) + + # NOTE (cknight): Calling the versions API without + # /v1 or /v2 in the URL will lead to this unversioned + # method, which should always return info about all + # available versions. + @wsgi.response(300) + def all(self, req): + """Return all known versions.""" builder = views_versions.get_view_builder(req) known_versions = copy.deepcopy(_KNOWN_VERSIONS) return builder.build_versions(known_versions) diff --git a/manila/api/views/shares.py b/manila/api/views/shares.py index 286f0c85..4a6ffb83 100644 --- a/manila/api/views/shares.py +++ b/manila/api/views/shares.py @@ -85,7 +85,7 @@ class ViewBuilder(common.ViewBuilder): share_dict['share_server_id'] = share.get('share_server_id') return {'share': share_dict} - @common.ViewBuilder.versioned_method("1.5") + @common.ViewBuilder.versioned_method("2.4") def add_consistency_group_fields(self, share_dict, share): share_dict['consistency_group_id'] = share.get( 'consistency_group_id') diff --git a/manila/api/views/versions.py b/manila/api/views/versions.py index b3da20c6..ce26378f 100644 --- a/manila/api/views/versions.py +++ b/manila/api/views/versions.py @@ -24,6 +24,9 @@ def get_view_builder(req): return ViewBuilder(req.application_url) +_URL_SUFFIX = {'v1.0': 'v1', 'v2.0': 'v2'} + + class ViewBuilder(object): def __init__(self, base_url): """Initialize ViewBuilder. @@ -45,7 +48,9 @@ class ViewBuilder(object): def _build_links(self, version_data): """Generate a container of links that refer to the provided version.""" links = copy.deepcopy(version_data.get('links', {})) - links.append({'rel': 'self', 'href': self._generate_href()}) + version = _URL_SUFFIX.get(version_data['id']) + links.append({'rel': 'self', + 'href': self._generate_href(version=version)}) return links def _generate_href(self, version='v1', path=None): diff --git a/manila/tests/api/contrib/test_admin_actions.py b/manila/tests/api/contrib/test_admin_actions.py index aeaa470c..16e269c0 100644 --- a/manila/tests/api/contrib/test_admin_actions.py +++ b/manila/tests/api/contrib/test_admin_actions.py @@ -36,7 +36,7 @@ def app(): # no auth, just let environ['manila.context'] pass through api = fakes.router.APIRouter() mapper = fakes.urlmap.URLMap() - mapper['/v1'] = api + mapper['/v2'] = api return mapper @@ -76,7 +76,7 @@ class AdminActionsTest(test.TestCase): share = db_utils.create_share(status=constants.STATUS_AVAILABLE, size='1', override_defaults=True) - req = webob.Request.blank('/v1/fake/shares/%s/action' % share['id']) + req = webob.Request.blank('/v2/fake/shares/%s/action' % share['id']) return share, req def _setup_snapshot_data(self, snapshot=None): @@ -84,7 +84,7 @@ class AdminActionsTest(test.TestCase): share = db_utils.create_share() snapshot = db_utils.create_snapshot( status=constants.STATUS_AVAILABLE, share_id=share['id']) - req = webob.Request.blank('/v1/fake/snapshots/%s/action' % + req = webob.Request.blank('/v2/fake/snapshots/%s/action' % snapshot['id']) return snapshot, req @@ -93,16 +93,16 @@ class AdminActionsTest(test.TestCase): instance = db_utils.create_share(status=constants.STATUS_AVAILABLE, size='1').instance req = webob.Request.blank( - '/v1/fake/share_instances/%s/action' % instance['id']) + '/v2/fake/share_instances/%s/action' % instance['id']) return instance, req def _setup_cg_data(self, cg=None): if cg is None: cg = db_utils.create_consistency_group( status=constants.STATUS_AVAILABLE) - req = webob.Request.blank('/v1/fake/consistency-groups/%s/action' % + req = webob.Request.blank('/v2/fake/consistency-groups/%s/action' % cg['id']) - req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '1.5' + req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '2.4' req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True' return cg, req @@ -111,9 +111,9 @@ class AdminActionsTest(test.TestCase): if cgsnapshot is None: cgsnapshot = db_utils.create_cgsnapshot( 'fake_id', status=constants.STATUS_AVAILABLE) - req = webob.Request.blank('/v1/fake/cgsnapshots/%s/action' % + req = webob.Request.blank('/v2/fake/cgsnapshots/%s/action' % cgsnapshot['id']) - req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '1.5' + req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '2.4' req.headers[wsgi.EXPERIMENTAL_API_REQUEST_HEADER] = 'True' return cgsnapshot, req diff --git a/manila/tests/api/test_versions.py b/manila/tests/api/test_versions.py index 7f620623..0045d24d 100644 --- a/manila/tests/api/test_versions.py +++ b/manila/tests/api/test_versions.py @@ -36,94 +36,81 @@ class VersionsControllerTestCase(test.TestCase): super(VersionsControllerTestCase, self).setUp() self.wsgi_apps = (versions.VersionsRouter(), router.APIRouter()) - @ddt.data(('', 302), ('/', 200)) - @ddt.unpack - def test_versions_return_codes(self, request_path, return_code): - req = fakes.HTTPRequest.blank(request_path) + @ddt.data('1.0', '1.1', '2.0', '3.0') + def test_versions_root(self, version): + req = fakes.HTTPRequest.blank('/', base_url='http://localhost') req.method = 'GET' req.content_type = 'application/json' + req.headers = {version_header_name: version} - for app in self.wsgi_apps: - response = req.get_response(app) - self.assertEqual(return_code, response.status_int) + response = req.get_response(versions.VersionsRouter()) + self.assertEqual(300, response.status_int) + body = jsonutils.loads(response.body) + version_list = body['versions'] - @ddt.data( - ('http://localhost/', True), - (None, True), - ('http://localhost/', False), - (None, False), - ) - @ddt.unpack - def test_versions_index_v10(self, base_url, include_header): - req = fakes.HTTPRequest.blank('/', base_url=base_url) + ids = [v['id'] for v in version_list] + self.assertEqual({'v1.0', 'v2.0'}, set(ids)) + self.assertNotIn(version_header_name, response.headers) + self.assertNotIn('Vary', response.headers) + + v1 = [v for v in version_list if v['id'] == 'v1.0'][0] + self.assertEqual('', v1.get('min_version')) + self.assertEqual('', v1.get('version')) + + v2 = [v for v in version_list if v['id'] == 'v2.0'][0] + self.assertEqual(api_version_request._MIN_API_VERSION, + v2.get('min_version')) + self.assertEqual(api_version_request._MAX_API_VERSION, + v2.get('version')) + + @ddt.data('1.0', + '1.1', + api_version_request._MIN_API_VERSION, + api_version_request._MAX_API_VERSION) + def test_versions_v1(self, version): + req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v1') req.method = 'GET' req.content_type = 'application/json' - if include_header: - req.headers = {version_header_name: '1.0'} + req.headers = {version_header_name: version} - for app in self.wsgi_apps: - response = req.get_response(app) - body = jsonutils.loads(response.body) - version_list = body['versions'] + response = req.get_response(router.APIRouter()) + self.assertEqual(200, response.status_int) + body = jsonutils.loads(response.body) + version_list = body['versions'] - ids = [v['id'] for v in version_list] - self.assertEqual({'v1.0'}, set(ids)) - self.assertEqual('1.0', response.headers[version_header_name]) - self.assertEqual(version_header_name, response.headers['Vary']) - self.assertIsNone(version_list[0].get('min_version')) - self.assertIsNone(version_list[0].get('version')) + ids = [v['id'] for v in version_list] + self.assertEqual({'v1.0'}, set(ids)) + self.assertEqual('1.0', response.headers[version_header_name]) + self.assertEqual(version_header_name, response.headers['Vary']) + self.assertEqual('', version_list[0].get('min_version')) + self.assertEqual('', version_list[0].get('version')) - @ddt.data( - ('http://localhost/', '1.1'), - (None, '1.1'), - ('http://localhost/', 'latest'), - (None, 'latest') - ) - @ddt.unpack - def test_versions_index_v11(self, base_url, req_version): - req = fakes.HTTPRequest.blank('/', base_url=base_url) + @ddt.data(api_version_request._MIN_API_VERSION, + api_version_request._MAX_API_VERSION) + def test_versions_v2(self, version): + req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2') req.method = 'GET' req.content_type = 'application/json' - req.headers = {version_header_name: req_version} + req.headers = {version_header_name: version} - for app in self.wsgi_apps: - response = req.get_response(app) - body = jsonutils.loads(response.body) - version_list = body['versions'] + response = req.get_response(router.APIRouter()) + self.assertEqual(200, response.status_int) + body = jsonutils.loads(response.body) + version_list = body['versions'] - ids = [v['id'] for v in version_list] - self.assertEqual({'v1.0'}, set(ids)) + ids = [v['id'] for v in version_list] + self.assertEqual({'v2.0'}, set(ids)) + self.assertEqual(version, response.headers[version_header_name]) + self.assertEqual(version_header_name, response.headers['Vary']) - if req_version == 'latest': - self.assertEqual(api_version_request._MAX_API_VERSION, - response.headers[version_header_name]) - else: - self.assertEqual(req_version, - response.headers[version_header_name]) + v2 = [v for v in version_list if v['id'] == 'v2.0'][0] + self.assertEqual(api_version_request._MIN_API_VERSION, + v2.get('min_version')) + self.assertEqual(api_version_request._MAX_API_VERSION, + v2.get('version')) - self.assertEqual(version_header_name, response.headers['Vary']) - self.assertEqual(api_version_request._MIN_API_VERSION, - version_list[0].get('min_version')) - self.assertEqual(api_version_request._MAX_API_VERSION, - version_list[0].get('version')) - - @ddt.data('http://localhost/', None) - def test_versions_index_v2(self, base_url): - req = fakes.HTTPRequest.blank('/', base_url=base_url) - req.method = 'GET' - req.content_type = 'application/json' - req.headers = {version_header_name: '2.0'} - - for app in self.wsgi_apps: - response = req.get_response(app) - - self.assertEqual(406, response.status_int) - self.assertEqual('2.0', response.headers[version_header_name]) - self.assertEqual(version_header_name, response.headers['Vary']) - - @ddt.data('http://localhost/', None) - def test_versions_index_invalid_version_request(self, base_url): - req = fakes.HTTPRequest.blank('/', base_url=base_url) + def test_versions_version_invalid(self): + req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2') req.method = 'GET' req.content_type = 'application/json' req.headers = {version_header_name: '2.0.1'} @@ -132,8 +119,6 @@ class VersionsControllerTestCase(test.TestCase): response = req.get_response(app) self.assertEqual(400, response.status_int) - self.assertEqual('1.0', response.headers[version_header_name]) - self.assertEqual(version_header_name, response.headers['Vary']) def test_versions_version_not_found(self): api_version_request_3_0 = api_version_request.APIVersionRequest('3.0') @@ -142,45 +127,59 @@ class VersionsControllerTestCase(test.TestCase): mock.Mock(return_value=api_version_request_3_0)) class Controller(wsgi.Controller): - @wsgi.Controller.api_version('1.0', '1.0') + @wsgi.Controller.api_version('2.0', '2.0') def index(self, req): return 'off' - req = fakes.HTTPRequest.blank('/tests') - req.headers = {version_header_name: '2.0'} + req = fakes.HTTPRequest.blank('/tests', base_url='http://localhost/v2') + req.headers = {version_header_name: '2.5'} app = fakes.TestRouter(Controller()) + response = req.get_response(app) self.assertEqual(404, response.status_int) + def test_versions_version_not_acceptable(self): + req = fakes.HTTPRequest.blank('/', base_url='http://localhost/v2') + req.method = 'GET' + req.content_type = 'application/json' + req.headers = {version_header_name: '3.0'} + + response = req.get_response(router.APIRouter()) + + self.assertEqual(406, response.status_int) + self.assertEqual('3.0', response.headers[version_header_name]) + self.assertEqual(version_header_name, response.headers['Vary']) + @ddt.ddt class ExperimentalAPITestCase(test.TestCase): class Controller(wsgi.Controller): - @wsgi.Controller.api_version('1.0', '1.0') + @wsgi.Controller.api_version('2.0', '2.0') def index(self, req): return {'fake_key': 'fake_value'} - @wsgi.Controller.api_version('1.1', '1.1', experimental=True) # noqa + @wsgi.Controller.api_version('2.1', '2.1', experimental=True) # noqa def index(self, req): # pylint: disable=E0102 return {'fake_key': 'fake_value'} def setUp(self): super(ExperimentalAPITestCase, self).setUp() self.app = fakes.TestRouter(ExperimentalAPITestCase.Controller()) + self.req = fakes.HTTPRequest.blank('/tests', + base_url='http://localhost/v2') @ddt.data(True, False) def test_stable_api_always_called(self, experimental): - req = fakes.HTTPRequest.blank('/tests') - req.headers = {version_header_name: '1.0'} + self.req.headers = {version_header_name: '2.0'} if experimental: - req.headers[experimental_header_name] = experimental - response = req.get_response(self.app) + self.req.headers[experimental_header_name] = experimental + response = self.req.get_response(self.app) self.assertEqual(200, response.status_int) - self.assertEqual('1.0', response.headers[version_header_name]) + self.assertEqual('2.0', response.headers[version_header_name]) if experimental: self.assertEqual(experimental, @@ -190,22 +189,20 @@ class ExperimentalAPITestCase(test.TestCase): def test_experimental_api_called_when_requested(self): - req = fakes.HTTPRequest.blank('/tests') - req.headers = { - version_header_name: '1.1', + self.req.headers = { + version_header_name: '2.1', experimental_header_name: 'True', } - response = req.get_response(self.app) + response = self.req.get_response(self.app) self.assertEqual(200, response.status_int) - self.assertEqual('1.1', response.headers[version_header_name]) + self.assertEqual('2.1', response.headers[version_header_name]) self.assertTrue(response.headers.get(experimental_header_name)) def test_experimental_api_not_called_when_not_requested(self): - req = fakes.HTTPRequest.blank('/tests') - req.headers = {version_header_name: '1.1'} - response = req.get_response(self.app) + self.req.headers = {version_header_name: '2.1'} + response = self.req.get_response(self.app) self.assertEqual(404, response.status_int) self.assertFalse(experimental_header_name in response.headers) @@ -217,12 +214,11 @@ class ExperimentalAPITestCase(test.TestCase): 'max_api_version', mock.Mock(return_value=api_version_request_3_0)) - req = fakes.HTTPRequest.blank('/tests') - req.headers = { - version_header_name: '1.2', + self.req.headers = { + version_header_name: '2.2', experimental_header_name: 'True', } - response = req.get_response(self.app) + response = self.req.get_response(self.app) self.assertEqual(404, response.status_int) self.assertTrue(response.headers.get(experimental_header_name)) \ No newline at end of file diff --git a/manila/tests/api/v1/test_cgsnapshots.py b/manila/tests/api/v1/test_cgsnapshots.py index 574c2a2d..1c575caa 100644 --- a/manila/tests/api/v1/test_cgsnapshots.py +++ b/manila/tests/api/v1/test_cgsnapshots.py @@ -36,7 +36,7 @@ class CGSnapshotApiTest(test.TestCase): def setUp(self): super(CGSnapshotApiTest, self).setUp() self.controller = cgs.CGSnapshotController() - self.api_version = '1.5' + self.api_version = '2.4' self.request = fakes.HTTPRequest.blank('/consistency-groups', version=self.api_version, experimental=True) diff --git a/manila/tests/api/v1/test_consistency_groups.py b/manila/tests/api/v1/test_consistency_groups.py index a74b8c43..c339cc38 100644 --- a/manila/tests/api/v1/test_consistency_groups.py +++ b/manila/tests/api/v1/test_consistency_groups.py @@ -40,7 +40,7 @@ class CGApiTest(test.TestCase): super(CGApiTest, self).setUp() self.controller = cgs.CGController() self.fake_share_type = {'id': six.text_type(uuid.uuid4())} - self.api_version = '1.5' + self.api_version = '2.4' self.request = fakes.HTTPRequest.blank('/consistency-groups', version=self.api_version, experimental=True) diff --git a/manila/tests/api/v1/test_share_instances.py b/manila/tests/api/v1/test_share_instances.py index 0cdc7b87..d628f2ab 100644 --- a/manila/tests/api/v1/test_share_instances.py +++ b/manila/tests/api/v1/test_share_instances.py @@ -34,7 +34,7 @@ class ShareInstancesApiTest(test.TestCase): def _get_request(self, uri, context=None): if context is None: context = self.context - req = fakes.HTTPRequest.blank('/shares', version="1.4") + req = fakes.HTTPRequest.blank('/shares', version="2.3") req.environ['manila.context'] = context return req diff --git a/manila/tests/api/v1/test_shares.py b/manila/tests/api/v1/test_shares.py index f47f6a42..46779d8e 100644 --- a/manila/tests/api/v1/test_shares.py +++ b/manila/tests/api/v1/test_shares.py @@ -142,7 +142,7 @@ class ShareApiTest(test.TestCase): self.mock_object(share_api.API, 'create', self.create_mock) body = {"share": copy.deepcopy(self.share)} - req = fakes.HTTPRequest.blank('/shares', version="1.5") + req = fakes.HTTPRequest.blank('/shares', version="2.4") res_dict = self.controller.create(req, body) expected = self._get_expected_share_detailed_response(self.share) @@ -213,7 +213,7 @@ class ShareApiTest(test.TestCase): use_admin_context=True) req.method = 'POST' req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('1.6') + 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') @@ -224,7 +224,7 @@ class ShareApiTest(test.TestCase): use_admin_context=True) req.method = 'POST' req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('1.6') + 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') @@ -240,7 +240,7 @@ class ShareApiTest(test.TestCase): use_admin_context=True) req.method = 'POST' req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('1.6') + 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') @@ -254,7 +254,7 @@ class ShareApiTest(test.TestCase): use_admin_context=True) req.method = 'POST' req.headers['content-type'] = 'application/json' - req.api_version_request = api_version.APIVersionRequest('1.6') + 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'}} @@ -418,7 +418,7 @@ class ShareApiTest(test.TestCase): self.assertEqual(expected, res_dict) def test_share_show_with_consistency_group(self): - req = fakes.HTTPRequest.blank('/shares/1', version='1.5') + req = fakes.HTTPRequest.blank('/shares/1', version='2.4') res_dict = self.controller.show(req, '1') expected = self._get_expected_share_detailed_response() expected['share']['consistency_group_id'] = None @@ -489,7 +489,7 @@ class ShareApiTest(test.TestCase): shr = self.share body = {"share": shr} - req = fakes.HTTPRequest.blank('/share/1', version="1.5") + 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"]) @@ -732,7 +732,7 @@ class ShareApiTest(test.TestCase): stubs.stub_share_get_all_by_project) env = {'QUERY_STRING': 'name=Share+Test+Name'} req = fakes.HTTPRequest.blank('/shares/detail', environ=env, - version="1.5") + version="2.4") res_dict = self.controller.detail(req) expected = { 'shares': [ diff --git a/manila_tempest_tests/clients_share.py b/manila_tempest_tests/clients_share.py index 1411286a..65ee64ef 100644 --- a/manila_tempest_tests/clients_share.py +++ b/manila_tempest_tests/clients_share.py @@ -17,12 +17,16 @@ from tempest import clients from tempest.common import cred_provider from manila_tempest_tests.services.share.json import shares_client +from manila_tempest_tests.services.share.v2.json import shares_client \ + as shares_v2_client class Manager(clients.Manager): def __init__(self, credentials=None, service=None): super(Manager, self).__init__(credentials, service) self.shares_client = shares_client.SharesClient(self.auth_provider) + self.shares_v2_client = shares_v2_client.SharesV2Client( + self.auth_provider) class AltManager(Manager): diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index 046f8cdb..daed82e7 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -32,11 +32,11 @@ share_group = cfg.OptGroup(name="share", title="Share Service Options") ShareGroup = [ cfg.StrOpt("min_api_microversion", - default="1.0", + default="2.0", help="The minimum api microversion is configured to be the " "value of the minimum microversion supported by Manila."), cfg.StrOpt("max_api_microversion", - default="1.6", + default="2.5", 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/json/shares_client.py b/manila_tempest_tests/services/share/json/shares_client.py index 1cac0c0b..660f16b7 100644 --- a/manila_tempest_tests/services/share/json/shares_client.py +++ b/manila_tempest_tests/services/share/json/shares_client.py @@ -26,13 +26,6 @@ from tempest_lib import exceptions from manila_tempest_tests import share_exceptions CONF = config.CONF -LATEST_MICRO_API = { - 'X-OpenStack-Manila-API-Version': CONF.share.max_api_microversion, -} -EXPERIMENTAL = { - 'X-OpenStack-Manila-API-Experimental': 'True', - 'X-OpenStack-Manila-API-Version': CONF.share.max_api_microversion, -} class SharesClient(rest_client.RestClient): @@ -53,48 +46,11 @@ class SharesClient(rest_client.RestClient): self.share_network_id = CONF.share.share_network_id self.build_interval = CONF.share.build_interval self.build_timeout = CONF.share.build_timeout - self.API_MICROVERSIONS_HEADER = 'x-openstack-manila-api-version' - self.API_MICROVERSIONS_EXPERIMENTAL_HEADER = ( - 'x-openstack-manila-api-experimental') - - def _get_version_dict(self, version): - return {self.API_MICROVERSIONS_HEADER: version} - - def migrate_share(self, share_id, host): - post_body = { - 'os-migrate_share': { - 'host': host, - } - } - body = json.dumps(post_body) - headers = self._get_version_dict('1.6') - headers[self.API_MICROVERSIONS_EXPERIMENTAL_HEADER] = 'true' - return self.post('shares/%s/action' % share_id, body, - headers=headers, extra_headers=True) - - def send_microversion_request(self, version=None): - """Prepare and send the HTTP GET Request to the base URL. - - Extracts the base URL from the shares_client endpoint and makes a GET - request with the microversions request header. - """ - - headers = self.get_headers() - url, headers, body = self.auth_provider.auth_request( - 'GET', 'shares', headers, None, self.filters) - url = '/'.join(url.split('/')[:3]) + '/' - if version: - headers[self.API_MICROVERSIONS_HEADER] = version - resp, resp_body = self.raw_request(url, 'GET', headers=headers) - self.response_checker('GET', resp, resp_body) - resp_body = json.loads(resp_body) - return resp, resp_body def create_share(self, share_protocol=None, size=1, name=None, snapshot_id=None, description=None, metadata=None, share_network_id=None, - share_type_id=None, is_public=False, - consistency_group_id=None): + share_type_id=None, is_public=False): metadata = metadata or {} if name is None: name = data_utils.rand_name("tempest-created-share") @@ -119,18 +75,13 @@ class SharesClient(rest_client.RestClient): post_body["share"]["share_network_id"] = share_network_id if share_type_id: post_body["share"]["share_type"] = share_type_id - if consistency_group_id: - post_body["share"]["consistency_group_id"] = consistency_group_id body = json.dumps(post_body) - resp, body = self.post("shares", body, headers=LATEST_MICRO_API, - extra_headers=True) + resp, body = self.post("shares", body) self.expected_success(200, resp.status) return self._parse_resp(body) - def delete_share(self, share_id, params=None): - uri = "shares/%s" % share_id - uri += '?%s' % (urllib.urlencode(params) if params else '') - resp, body = self.delete(uri) + def delete_share(self, share_id): + resp, body = self.delete("shares/%s" % share_id) self.expected_success(202, resp.status) return body @@ -161,8 +112,7 @@ class SharesClient(rest_client.RestClient): """Get list of shares w/o filters.""" uri = 'shares/detail' if detailed else 'shares' uri += '?%s' % urllib.urlencode(params) if params else '' - resp, body = self.get(uri, headers=LATEST_MICRO_API, - extra_headers=True) + resp, body = self.get(uri) self.expected_success(200, resp.status) return self._parse_resp(body) @@ -171,29 +121,7 @@ class SharesClient(rest_client.RestClient): return self.list_shares(detailed=True, params=params) def get_share(self, share_id): - resp, body = self.get("shares/%s" % share_id, headers=LATEST_MICRO_API, - extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def get_instances_of_share(self, share_id): - resp, body = self.get("shares/%s/instances" % share_id, - headers=self._get_version_dict('1.4'), - extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def list_share_instances(self): - resp, body = self.get("share_instances", - headers=self._get_version_dict('1.4'), - extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def get_share_instance(self, instance_id): - resp, body = self.get("share_instances/%s" % instance_id, - headers=self._get_version_dict('1.4'), - extra_headers=True) + resp, body = self.get("shares/%s" % share_id) self.expected_success(200, resp.status) return self._parse_resp(body) @@ -292,30 +220,6 @@ class SharesClient(rest_client.RestClient): self.expected_success(202, resp.status) return body - def wait_for_migration_completed(self, share_id, dest_host): - """Waits for a share to migrate to a certain host.""" - share = self.get_share(share_id) - migration_timeout = CONF.share.migration_timeout - start = int(time.time()) - while share['task_state'] != 'migration_success': - time.sleep(self.build_interval) - share = self.get_share(share_id) - if share['task_state'] == 'migration_success': - return share - elif share['task_state'] == 'migration_error': - raise share_exceptions.ShareMigrationException( - share_id=share['id'], src=share['host'], dest=dest_host) - elif int(time.time()) - start >= migration_timeout: - message = ('Share %(share_id)s failed to migrate from ' - 'host %(src)s to host %(dest)s within the required ' - 'time %(timeout)s.' % { - 'src': share['host'], - 'dest': dest_host, - 'share_id': share['id'], - 'timeout': self.build_timeout - }) - raise exceptions.TimeoutException(message) - def wait_for_share_status(self, share_id, status): """Waits for a share to reach a given status.""" body = self.get_share(share_id) @@ -339,28 +243,6 @@ class SharesClient(rest_client.RestClient): (share_name, status, self.build_timeout)) raise exceptions.TimeoutException(message) - def wait_for_share_instance_status(self, instance_id, status): - """Waits for a share to reach a given status.""" - body = self.get_share_instance(instance_id) - instance_status = body['status'] - start = int(time.time()) - - while instance_status != status: - time.sleep(self.build_interval) - body = self.get_share(instance_id) - instance_status = body['status'] - if instance_status == status: - return - elif 'error' in instance_status.lower(): - raise share_exceptions.\ - ShareInstanceBuildErrorException(id=instance_id) - - if int(time.time()) - start >= self.build_timeout: - message = ('Share instance %s failed to reach %s status within' - ' the required time (%s s).' % - (instance_id, status, self.build_timeout)) - raise exceptions.TimeoutException(message) - def wait_for_snapshot_status(self, snapshot_id, status): """Waits for a snapshot to reach a given status.""" body = self.get_snapshot(snapshot_id) @@ -382,49 +264,6 @@ class SharesClient(rest_client.RestClient): (snapshot_name, status, self.build_timeout)) raise exceptions.TimeoutException(message) - def wait_for_consistency_group_status(self, consistency_group_id, status): - """Waits for a consistency group to reach a given status.""" - body = self.get_consistency_group(consistency_group_id) - consistency_group_name = body['name'] - consistency_group_status = body['status'] - start = int(time.time()) - - while consistency_group_status != status: - time.sleep(self.build_interval) - body = self.get_consistency_group(consistency_group_id) - consistency_group_status = body['status'] - if 'error' in consistency_group_status and status != 'error': - raise share_exceptions.ConsistencyGroupBuildErrorException( - consistency_group_id=consistency_group_id) - - if int(time.time()) - start >= self.build_timeout: - message = ('Consistency Group %s failed to reach %s status ' - 'within the required time (%s s).' % - (consistency_group_name, status, - self.build_timeout)) - raise exceptions.TimeoutException(message) - - def wait_for_cgsnapshot_status(self, cgsnapshot_id, status): - """Waits for a cgsnapshot to reach a given status.""" - body = self.get_cgsnapshot(cgsnapshot_id) - cgsnapshot_name = body['name'] - cgsnapshot_status = body['status'] - start = int(time.time()) - - while cgsnapshot_status != status: - time.sleep(self.build_interval) - body = self.get_cgsnapshot(cgsnapshot_id) - cgsnapshot_status = body['status'] - if 'error' in cgsnapshot_status and status != 'error': - raise share_exceptions.CGSnapshotBuildErrorException( - cgsnapshot_id=cgsnapshot_id) - - if int(time.time()) - start >= self.build_timeout: - message = ('CGSnapshot %s failed to reach %s status ' - 'within the required time (%s s).' % - (cgsnapshot_name, status, self.build_timeout)) - raise exceptions.TimeoutException(message) - def wait_for_access_rule_status(self, share_id, rule_id, status): """Waits for an access rule to reach a given status.""" rule_status = "new" @@ -502,8 +341,7 @@ class SharesClient(rest_client.RestClient): """Verifies whether provided resource deleted or not. :param kwargs: dict with expected keys 'share_id', 'snapshot_id', - :param kwargs: 'sn_id', 'ss_id', 'vt_id', 'server_id', 'cg_id', - :param kwargs: and 'cgsnapshot_id' + :param kwargs: 'sn_id', 'ss_id', 'vt_id' and 'server_id' :raises share_exceptions.InvalidResource """ if "share_id" in kwargs: @@ -518,9 +356,6 @@ class SharesClient(rest_client.RestClient): else: return self._is_resource_deleted( self.get_share, kwargs.get("share_id")) - elif "share_instance_id" in kwargs: - return self._is_resource_deleted( - self.get_share_instance, kwargs.get("share_instance_id")) elif "snapshot_id" in kwargs: return self._is_resource_deleted( self.get_snapshot, kwargs.get("snapshot_id")) @@ -539,12 +374,6 @@ class SharesClient(rest_client.RestClient): elif "server_id" in kwargs: return self._is_resource_deleted( self.show_share_server, kwargs.get("server_id")) - elif "cg_id" in kwargs: - return self._is_resource_deleted( - self.get_consistency_group, kwargs.get("cg_id")) - elif "cgsnapshot_id" in kwargs: - return self._is_resource_deleted( - self.get_cgsnapshot, kwargs.get("cgsnapshot_id")) else: raise share_exceptions.InvalidResource( message=six.text_type(kwargs)) @@ -598,29 +427,26 @@ class SharesClient(rest_client.RestClient): self.expected_success(200, resp.status) return self._parse_resp(body) - def reset_state(self, s_id, status="error", s_type="shares", - headers=None): - """Resets the state of a share, snapshot, cg, or a cgsnapshot. + def reset_state(self, s_id, status="error", s_type="shares"): + """Resets the state of a share or a snapshot. status: available, error, creating, deleting, error_deleting - s_type: shares, snapshots, consistency-groups, cgsnapshots + s_type: shares, snapshots """ body = {"os-reset_status": {"status": status}} body = json.dumps(body) - resp, body = self.post("%s/%s/action" % (s_type, s_id), body, - headers=headers, extra_headers=True) + resp, body = self.post("%s/%s/action" % (s_type, s_id), body) self.expected_success(202, resp.status) return body - def force_delete(self, s_id, s_type="shares", headers=None): + def force_delete(self, s_id, s_type="shares"): """Force delete share or snapshot. s_type: shares, snapshots """ body = {"os-force_delete": None} body = json.dumps(body) - resp, body = self.post("%s/%s/action" % (s_type, s_id), body, - headers=headers, extra_headers=True) + resp, body = self.post("%s/%s/action" % (s_type, s_id), body) self.expected_success(202, resp.status) return body @@ -925,143 +751,3 @@ class SharesClient(rest_client.RestClient): resp, body = self.get(uri) self.expected_success(200, resp.status) return json.loads(body) - -############### - - def create_consistency_group(self, name=None, description=None, - share_type_ids=(), share_network_id=None, - source_cgsnapshot_id=None): - """Create a new consistency group.""" - uri = 'consistency-groups' - post_body = {} - if name: - post_body['name'] = name - if description: - post_body['description'] = description - if share_type_ids: - post_body['share_types'] = share_type_ids - if source_cgsnapshot_id: - post_body['source_cgsnapshot_id'] = source_cgsnapshot_id - if share_network_id: - post_body['share_network_id'] = share_network_id - body = json.dumps({'consistency_group': post_body}) - resp, body = self.post(uri, body, headers=EXPERIMENTAL, - extra_headers=True) - self.expected_success(202, resp.status) - return self._parse_resp(body) - - def delete_consistency_group(self, consistency_group_id): - """Delete a consistency group.""" - uri = 'consistency-groups/%s' % consistency_group_id - resp, body = self.delete(uri, headers=EXPERIMENTAL, - extra_headers=True) - self.expected_success(202, resp.status) - return body - - def list_consistency_groups(self, detailed=False, params=None): - """Get list of consistency groups w/o filters.""" - uri = 'consistency-groups%s' % ('/detail' if detailed else '') - uri += '?%s' % (urllib.urlencode(params) if params else '') - resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def get_consistency_group(self, consistency_group_id): - """Get consistency group info.""" - uri = 'consistency-groups/%s' % consistency_group_id - resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def update_consistency_group(self, consistency_group_id, name=None, - description=None, **kwargs): - """Update an existing consistency group.""" - uri = 'consistency-groups/%s' % consistency_group_id - post_body = {} - if name: - post_body['name'] = name - if description: - post_body['description'] = description - if kwargs: - post_body.update(kwargs) - body = json.dumps({'consistency_group': post_body}) - resp, body = self.put(uri, body, headers=EXPERIMENTAL, - extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def consistency_group_reset_state(self, id, status): - self.reset_state(id, status=status, - s_type='consistency-groups', headers=EXPERIMENTAL) - - def consistency_group_force_delete(self, id, status): - self.force_delete(id, status=status, - s_type='consistency-groups', headers=EXPERIMENTAL) - -############### - - def create_cgsnapshot(self, consistency_group_id, - name=None, description=None): - """Create a new cgsnapshot of an existing consistency group.""" - uri = 'cgsnapshots' - post_body = {'consistency_group_id': consistency_group_id} - if name: - post_body['name'] = name - if description: - post_body['description'] = description - body = json.dumps({'cgsnapshot': post_body}) - resp, body = self.post(uri, body, headers=EXPERIMENTAL, - extra_headers=True) - self.expected_success(202, resp.status) - return self._parse_resp(body) - - def delete_cgsnapshot(self, cgsnapshot_id): - """Delete an existing cgsnapshot.""" - uri = 'cgsnapshots/%s' % cgsnapshot_id - resp, body = self.delete(uri, headers=EXPERIMENTAL, extra_headers=True) - self.expected_success(202, resp.status) - return body - - def list_cgsnapshots(self, detailed=False, params=None): - """Get list of cgsnapshots w/o filters.""" - uri = 'cgsnapshots/detail' if detailed else 'cgsnapshots' - uri += '?%s' % (urllib.urlencode(params) if params else '') - resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def list_cgsnapshot_members(self, cgsnapshot_id): - """Get list of members of a cgsnapshots.""" - uri = 'cgsnapshots/%s/members' % cgsnapshot_id - resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def get_cgsnapshot(self, cgsnapshot_id): - """Get cgsnapshot info.""" - uri = 'cgsnapshots/%s' % cgsnapshot_id - resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def update_cgsnapshot(self, cgsnapshot_id, name=None, description=None): - """Update an existing cgsnapshot.""" - uri = 'cgsnapshots/%s' % cgsnapshot_id - post_body = {} - if name: - post_body['name'] = name - if description: - post_body['description'] = description - body = json.dumps({'cgsnapshot': post_body}) - resp, body = self.put(uri, body, headers=EXPERIMENTAL, - extra_headers=True) - self.expected_success(200, resp.status) - return self._parse_resp(body) - - def cgsnapshot_reset_state(self, id, status): - self.reset_state(id, status=status, - s_type='cgsnapshots', headers=EXPERIMENTAL) - - def cgsnapshot_force_delete(self, id, status): - self.force_delete(id, status=status, - s_type='cgsnapshots', headers=EXPERIMENTAL) diff --git a/manila_tempest_tests/services/share/v2/__init__.py b/manila_tempest_tests/services/share/v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_tempest_tests/services/share/v2/json/__init__.py b/manila_tempest_tests/services/share/v2/json/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py new file mode 100644 index 00000000..da72ffc1 --- /dev/null +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -0,0 +1,516 @@ +# Copyright 2015 Andrew Kerr +# 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 json +import time +import urllib + +from tempest_lib.common.utils import data_utils +from tempest_lib import exceptions + +from manila_tempest_tests.services.share.json import shares_client # noqa +from manila_tempest_tests import share_exceptions +from tempest import config # noqa + +CONF = config.CONF +LATEST_MICROVERSION = CONF.share.max_api_microversion +EXPERIMENTAL = {'X-OpenStack-Manila-API-Experimental': 'True'} + + +class SharesV2Client(shares_client.SharesClient): + """Tempest REST client for Manila. + + It handles shares and access to it in OpenStack. + """ + api_version = 'v2' + + def __init__(self, auth_provider): + super(SharesV2Client, self).__init__(auth_provider) + self.API_MICROVERSIONS_HEADER = 'x-openstack-manila-api-version' + + def inject_microversion_header(self, headers, version, + extra_headers=False): + """Inject the required manila microversion header.""" + new_headers = self.get_headers() + new_headers[self.API_MICROVERSIONS_HEADER] = version + if extra_headers and headers: + new_headers.update(headers) + elif headers: + new_headers = headers + return new_headers + + # Overwrite all http verb calls to inject the micro version header + def post(self, url, body, headers=None, extra_headers=False, + version=LATEST_MICROVERSION): + headers = self.inject_microversion_header(headers, version, + extra_headers=extra_headers) + return super(SharesV2Client, self).post(url, body, headers=headers) + + def get(self, url, headers=None, extra_headers=False, + version=LATEST_MICROVERSION): + headers = self.inject_microversion_header(headers, version, + extra_headers=extra_headers) + return super(SharesV2Client, self).get(url, headers=headers) + + def delete(self, url, headers=None, body=None, extra_headers=False, + version=LATEST_MICROVERSION): + headers = self.inject_microversion_header(headers, version, + extra_headers=extra_headers) + return super(SharesV2Client, self).delete(url, headers=headers, + body=body) + + def patch(self, url, body, headers=None, extra_headers=False, + version=LATEST_MICROVERSION): + headers = self.inject_microversion_header(headers, version, + extra_headers=extra_headers) + return super(SharesV2Client, self).patch(url, body, headers=headers) + + def put(self, url, body, headers=None, extra_headers=False, + version=LATEST_MICROVERSION): + headers = self.inject_microversion_header(headers, version, + extra_headers=extra_headers) + return super(SharesV2Client, self).put(url, body, headers=headers) + + def head(self, url, headers=None, extra_headers=False, + version=LATEST_MICROVERSION): + headers = self.inject_microversion_header(headers, version, + extra_headers=extra_headers) + return super(SharesV2Client, self).head(url, headers=headers) + + def copy(self, url, headers=None, extra_headers=False, + version=LATEST_MICROVERSION): + headers = self.inject_microversion_header(headers, version, + extra_headers=extra_headers) + return super(SharesV2Client, self).copy(url, headers=headers) + + def reset_state(self, s_id, status="error", s_type="shares", + headers=None, version=LATEST_MICROVERSION): + """Resets the state of a share, snapshot, cg, or a cgsnapshot. + + status: available, error, creating, deleting, error_deleting + s_type: shares, snapshots, consistency-groups, cgsnapshots + """ + body = {"os-reset_status": {"status": status}} + body = json.dumps(body) + resp, body = self.post("%s/%s/action" % (s_type, s_id), body, + headers=headers, extra_headers=True, + version=version) + self.expected_success(202, resp.status) + return body + + def force_delete(self, s_id, s_type="shares", headers=None, + version=LATEST_MICROVERSION): + """Force delete share or snapshot. + + s_type: shares, snapshots + """ + body = {"os-force_delete": None} + body = json.dumps(body) + resp, body = self.post("%s/%s/action" % (s_type, s_id), body, + headers=headers, extra_headers=True, + version=version) + self.expected_success(202, resp.status) + return body + + def send_microversion_request(self, version=None, script_name=None): + """Prepare and send the HTTP GET Request to the base URL. + + Extracts the base URL from the shares_client endpoint and makes a GET + request with the microversions request header. + :param version: The string to send for the value of the microversion + header, or None to omit the header. + :param script_name: The first part of the URL (v1 or v2), or None to + omit it. + """ + + headers = self.get_headers() + url, headers, body = self.auth_provider.auth_request( + 'GET', 'shares', headers, None, self.filters) + url = '/'.join(url.split('/')[:3]) + '/' + if script_name: + url += script_name + '/' + if version: + headers[self.API_MICROVERSIONS_HEADER] = version + resp, resp_body = self.raw_request(url, 'GET', headers=headers) + self.response_checker('GET', resp, resp_body) + resp_body = json.loads(resp_body) + return resp, resp_body + + def is_resource_deleted(self, *args, **kwargs): + """Verifies whether provided resource deleted or not. + + :param kwargs: dict with expected keys 'share_id', 'snapshot_id', + :param kwargs: 'sn_id', 'ss_id', 'vt_id' and 'server_id' + :raises share_exceptions.InvalidResource + """ + if "share_instance_id" in kwargs: + return self._is_resource_deleted( + self.get_share_instance, kwargs.get("share_instance_id")) + elif "cg_id" in kwargs: + return self._is_resource_deleted( + self.get_consistency_group, kwargs.get("cg_id")) + elif "cgsnapshot_id" in kwargs: + return self._is_resource_deleted( + self.get_cgsnapshot, kwargs.get("cgsnapshot_id")) + else: + return super(SharesV2Client, self).is_resource_deleted( + *args, **kwargs) + +############### + + def create_share(self, share_protocol=None, size=1, + name=None, snapshot_id=None, description=None, + metadata=None, share_network_id=None, + share_type_id=None, is_public=False, + consistency_group_id=None, version=LATEST_MICROVERSION): + metadata = metadata or {} + if name is None: + name = data_utils.rand_name("tempest-created-share") + if description is None: + description = data_utils.rand_name("tempest-created-share-desc") + if share_protocol is None: + share_protocol = self.share_protocol + if share_protocol is None: + raise share_exceptions.ShareProtocolNotSpecified() + post_body = { + "share": { + "share_proto": share_protocol, + "description": description, + "snapshot_id": snapshot_id, + "name": name, + "size": size, + "metadata": metadata, + "is_public": is_public, + } + } + if share_network_id: + post_body["share"]["share_network_id"] = share_network_id + if share_type_id: + post_body["share"]["share_type"] = share_type_id + if consistency_group_id: + post_body["share"]["consistency_group_id"] = consistency_group_id + body = json.dumps(post_body) + resp, body = self.post("shares", body, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def list_shares(self, detailed=False, params=None, + version=LATEST_MICROVERSION): + """Get list of shares w/o filters.""" + uri = 'shares/detail' if detailed else 'shares' + uri += '?%s' % urllib.urlencode(params) if params else '' + resp, body = self.get(uri, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def list_shares_with_detail(self, params=None, + version=LATEST_MICROVERSION): + """Get detailed list of shares w/o filters.""" + return self.list_shares(detailed=True, params=params, version=version) + + def get_share(self, share_id, version=LATEST_MICROVERSION): + resp, body = self.get("shares/%s" % share_id, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def delete_share(self, share_id, params=None, + version=LATEST_MICROVERSION): + uri = "shares/%s" % share_id + uri += '?%s' % (urllib.urlencode(params) if params else '') + resp, body = self.delete(uri, version=version) + self.expected_success(202, resp.status) + return body + +############### + + def get_instances_of_share(self, share_id, version=LATEST_MICROVERSION): + resp, body = self.get("shares/%s/instances" % share_id, + version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def list_share_instances(self, version=LATEST_MICROVERSION): + resp, body = self.get("share_instances", version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def get_share_instance(self, instance_id, version=LATEST_MICROVERSION): + resp, body = self.get("share_instances/%s" % instance_id, + version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def wait_for_share_instance_status(self, instance_id, status, + version=LATEST_MICROVERSION): + """Waits for a share to reach a given status.""" + body = self.get_share_instance(instance_id, version=version) + instance_status = body['status'] + start = int(time.time()) + + while instance_status != status: + time.sleep(self.build_interval) + body = self.get_share(instance_id) + instance_status = body['status'] + if instance_status == status: + return + elif 'error' in instance_status.lower(): + raise share_exceptions. \ + ShareInstanceBuildErrorException(id=instance_id) + + if int(time.time()) - start >= self.build_timeout: + message = ('Share instance %s failed to reach %s status within' + ' the required time (%s s).' % + (instance_id, status, self.build_timeout)) + raise exceptions.TimeoutException(message) + +############### + + def create_consistency_group(self, name=None, description=None, + share_type_ids=(), share_network_id=None, + source_cgsnapshot_id=None, + version=LATEST_MICROVERSION): + """Create a new consistency group.""" + uri = 'consistency-groups' + post_body = {} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + if share_type_ids: + post_body['share_types'] = share_type_ids + if source_cgsnapshot_id: + post_body['source_cgsnapshot_id'] = source_cgsnapshot_id + if share_network_id: + post_body['share_network_id'] = share_network_id + body = json.dumps({'consistency_group': post_body}) + resp, body = self.post(uri, body, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(202, resp.status) + return self._parse_resp(body) + + def delete_consistency_group(self, consistency_group_id, + version=LATEST_MICROVERSION): + """Delete a consistency group.""" + uri = 'consistency-groups/%s' % consistency_group_id + resp, body = self.delete(uri, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(202, resp.status) + return body + + def list_consistency_groups(self, detailed=False, params=None, + version=LATEST_MICROVERSION): + """Get list of consistency groups w/o filters.""" + uri = 'consistency-groups%s' % ('/detail' if detailed else '') + uri += '?%s' % (urllib.urlencode(params) if params else '') + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def get_consistency_group(self, consistency_group_id, + version=LATEST_MICROVERSION): + """Get consistency group info.""" + uri = 'consistency-groups/%s' % consistency_group_id + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def update_consistency_group(self, consistency_group_id, name=None, + description=None, + version=LATEST_MICROVERSION, **kwargs): + """Update an existing consistency group.""" + uri = 'consistency-groups/%s' % consistency_group_id + post_body = {} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + if kwargs: + post_body.update(kwargs) + body = json.dumps({'consistency_group': post_body}) + resp, body = self.put(uri, body, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def consistency_group_reset_state(self, id, status, + version=LATEST_MICROVERSION): + self.reset_state(id, status=status, + s_type='consistency-groups', headers=EXPERIMENTAL, + version=version) + + def consistency_group_force_delete(self, id, version=LATEST_MICROVERSION): + self.force_delete(id, s_type='consistency-groups', + headers=EXPERIMENTAL, version=version) + + def wait_for_consistency_group_status(self, consistency_group_id, status): + """Waits for a consistency group to reach a given status.""" + body = self.get_consistency_group(consistency_group_id) + consistency_group_name = body['name'] + consistency_group_status = body['status'] + start = int(time.time()) + + while consistency_group_status != status: + time.sleep(self.build_interval) + body = self.get_consistency_group(consistency_group_id) + consistency_group_status = body['status'] + if 'error' in consistency_group_status and status != 'error': + raise share_exceptions.ConsistencyGroupBuildErrorException( + consistency_group_id=consistency_group_id) + + if int(time.time()) - start >= self.build_timeout: + message = ('Consistency Group %s failed to reach %s status ' + 'within the required time (%s s).' % + (consistency_group_name, status, + self.build_timeout)) + raise exceptions.TimeoutException(message) + +############### + + def create_cgsnapshot(self, consistency_group_id, + name=None, description=None, + version=LATEST_MICROVERSION): + """Create a new cgsnapshot of an existing consistency group.""" + uri = 'cgsnapshots' + post_body = {'consistency_group_id': consistency_group_id} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + body = json.dumps({'cgsnapshot': post_body}) + resp, body = self.post(uri, body, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(202, resp.status) + return self._parse_resp(body) + + def delete_cgsnapshot(self, cgsnapshot_id, + version=LATEST_MICROVERSION): + """Delete an existing cgsnapshot.""" + uri = 'cgsnapshots/%s' % cgsnapshot_id + resp, body = self.delete(uri, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(202, resp.status) + return body + + def list_cgsnapshots(self, detailed=False, params=None, + version=LATEST_MICROVERSION): + """Get list of cgsnapshots w/o filters.""" + uri = 'cgsnapshots/detail' if detailed else 'cgsnapshots' + uri += '?%s' % (urllib.urlencode(params) if params else '') + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def list_cgsnapshot_members(self, cgsnapshot_id, + version=LATEST_MICROVERSION): + """Get list of members of a cgsnapshots.""" + uri = 'cgsnapshots/%s/members' % cgsnapshot_id + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def get_cgsnapshot(self, cgsnapshot_id, version=LATEST_MICROVERSION): + """Get cgsnapshot info.""" + uri = 'cgsnapshots/%s' % cgsnapshot_id + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True, + version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def update_cgsnapshot(self, cgsnapshot_id, name=None, description=None, + version=LATEST_MICROVERSION): + """Update an existing cgsnapshot.""" + uri = 'cgsnapshots/%s' % cgsnapshot_id + post_body = {} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + body = json.dumps({'cgsnapshot': post_body}) + resp, body = self.put(uri, body, headers=EXPERIMENTAL, + extra_headers=True, version=version) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def cgsnapshot_reset_state(self, id, status, + version=LATEST_MICROVERSION): + self.reset_state(id, status=status, + s_type='cgsnapshots', headers=EXPERIMENTAL, + version=version) + + def cgsnapshot_force_delete(self, id, version=LATEST_MICROVERSION): + self.force_delete(id, s_type='cgsnapshots', headers=EXPERIMENTAL, + version=version) + + def wait_for_cgsnapshot_status(self, cgsnapshot_id, status): + """Waits for a cgsnapshot to reach a given status.""" + body = self.get_cgsnapshot(cgsnapshot_id) + cgsnapshot_name = body['name'] + cgsnapshot_status = body['status'] + start = int(time.time()) + + while cgsnapshot_status != status: + time.sleep(self.build_interval) + body = self.get_cgsnapshot(cgsnapshot_id) + cgsnapshot_status = body['status'] + if 'error' in cgsnapshot_status and status != 'error': + raise share_exceptions.CGSnapshotBuildErrorException( + cgsnapshot_id=cgsnapshot_id) + + if int(time.time()) - start >= self.build_timeout: + message = ('CGSnapshot %s failed to reach %s status ' + 'within the required time (%s s).' % + (cgsnapshot_name, status, self.build_timeout)) + raise exceptions.TimeoutException(message) + +############### + + def migrate_share(self, share_id, host, version=LATEST_MICROVERSION): + post_body = { + 'os-migrate_share': { + 'host': host, + } + } + body = json.dumps(post_body) + return self.post('shares/%s/action' % share_id, body, + headers=EXPERIMENTAL, extra_headers=True, + version=version) + + def wait_for_migration_completed(self, share_id, dest_host): + """Waits for a share to migrate to a certain host.""" + share = self.get_share(share_id) + migration_timeout = CONF.share.migration_timeout + start = int(time.time()) + while share['task_state'] != 'migration_success': + time.sleep(self.build_interval) + share = self.get_share(share_id) + if share['task_state'] == 'migration_success': + return share + elif share['task_state'] == 'migration_error': + raise share_exceptions.ShareMigrationException( + share_id=share['id'], src=share['host'], dest=dest_host) + elif int(time.time()) - start >= migration_timeout: + message = ('Share %(share_id)s failed to migrate from ' + 'host %(src)s to host %(dest)s within the required ' + 'time %(timeout)s.' % { + 'src': share['host'], + 'dest': dest_host, + 'share_id': share['id'], + 'timeout': self.build_timeout + }) + raise exceptions.TimeoutException(message) diff --git a/manila_tempest_tests/tests/api/admin/test_admin_actions.py b/manila_tempest_tests/tests/api/admin/test_admin_actions.py index 6acec11f..e70fd6ae 100644 --- a/manila_tempest_tests/tests/api/admin/test_admin_actions.py +++ b/manila_tempest_tests/tests/api/admin/test_admin_actions.py @@ -32,7 +32,7 @@ class AdminActionsTest(base.BaseSharesAdminTest): cls.bad_status = "error_deleting" cls.sh = cls.create_share() cls.sh_instance = ( - cls.shares_client.get_instances_of_share(cls.sh["id"])[0] + cls.shares_v2_client.get_instances_of_share(cls.sh["id"])[0] ) if CONF.share.run_snapshot_tests: cls.sn = cls.create_snapshot_wait_for_active(cls.sh["id"]) @@ -49,7 +49,7 @@ class AdminActionsTest(base.BaseSharesAdminTest): for status in self.states: self.shares_client.reset_state( id, s_type="share_instances", status=status) - self.shares_client.wait_for_share_instance_status(id, status) + self.shares_v2_client.wait_for_share_instance_status(id, status) @test.attr(type=["gate", ]) @testtools.skipUnless(CONF.share.run_snapshot_tests, @@ -78,7 +78,7 @@ class AdminActionsTest(base.BaseSharesAdminTest): @test.attr(type=["gate", ]) def test_force_delete_share_instance(self): share = self.create_share(cleanup_in_class=False) - instances = self.shares_client.get_instances_of_share(share["id"]) + instances = self.shares_v2_client.get_instances_of_share(share["id"]) # Check that instance was created self.assertEqual(1, len(instances)) @@ -89,13 +89,13 @@ class AdminActionsTest(base.BaseSharesAdminTest): instance["id"], s_type="share_instances", status=self.bad_status) # Check that status was changed - check_status = self.shares_client.get_share_instance(instance["id"]) + check_status = self.shares_v2_client.get_share_instance(instance["id"]) self.assertEqual(self.bad_status, check_status["status"]) # Share with status 'error_deleting' should be deleted self.shares_client.force_delete( instance["id"], s_type="share_instances") - self.shares_client.wait_for_resource_deletion( + self.shares_v2_client.wait_for_resource_deletion( share_instance_id=instance["id"]) @test.attr(type=["gate", ]) diff --git a/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py b/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py index 7df3b30e..d25dc750 100644 --- a/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py +++ b/manila_tempest_tests/tests/api/admin/test_admin_actions_negative.py @@ -31,11 +31,12 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest): super(AdminActionsNegativeTest, cls).resource_setup() cls.sh = cls.create_share() cls.sh_instance = ( - cls.shares_client.get_instances_of_share(cls.sh["id"])[0] + cls.shares_v2_client.get_instances_of_share(cls.sh["id"])[0] ) if CONF.share.run_snapshot_tests: cls.sn = cls.create_snapshot_wait_for_active(cls.sh["id"]) cls.member_shares_client = clients.Manager().shares_client + cls.member_shares_v2_client = clients.Manager().shares_v2_client @test.attr(type=["gate", "negative", ]) def test_reset_nonexistent_share_state(self): @@ -149,19 +150,19 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest): def test_try_get_share_instance_with_member(self): # If a non-admin tries to get instance, it should be unauthorized self.assertRaises(lib_exc.Forbidden, - self.member_shares_client.get_share_instance, + self.member_shares_v2_client.get_share_instance, self.sh_instance["id"]) @test.attr(type=["gate", "negative", ]) def test_try_list_share_instance_with_member(self): # If a non-admin tries to list instances, it should be unauthorized self.assertRaises(lib_exc.Forbidden, - self.member_shares_client.list_share_instances) + self.member_shares_v2_client.list_share_instances) @test.attr(type=["gate", "negative", ]) def test_try_get_instances_of_share_with_member(self): # If a non-admin tries to list instances of given share, it should be # unauthorized self.assertRaises(lib_exc.Forbidden, - self.member_shares_client.get_instances_of_share, + self.member_shares_v2_client.get_instances_of_share, self.sh['id']) diff --git a/manila_tempest_tests/tests/api/admin/test_consistency_group_actions.py b/manila_tempest_tests/tests/api/admin/test_consistency_group_actions.py index c1fdb15c..d5142b6a 100644 --- a/manila_tempest_tests/tests/api/admin/test_consistency_group_actions.py +++ b/manila_tempest_tests/tests/api/admin/test_consistency_group_actions.py @@ -45,13 +45,19 @@ class ConsistencyGroupActionsTest(base.BaseSharesAdminTest): share_type_ids=[cls.share_type['id'], cls.share_type2['id']]) @test.attr(type=["gate", ]) - def test_create_cg_from_cgsnapshot_with_multiple_share_types(self): + def test_create_cg_from_cgsnapshot_with_multiple_share_types_v2_4(self): # Create cgsnapshot cgsnapshot = self.create_cgsnapshot_wait_for_active( - self.consistency_group["id"], cleanup_in_class=False) + self.consistency_group["id"], + cleanup_in_class=False, + version='2.4', + ) new_consistency_group = self.create_consistency_group( - cleanup_in_class=False, source_cgsnapshot_id=cgsnapshot['id']) + cleanup_in_class=False, + source_cgsnapshot_id=cgsnapshot['id'], + version='2.4', + ) # Verify share_types are the same expected_types = sorted(self.consistency_group['share_types']) @@ -61,7 +67,7 @@ class ConsistencyGroupActionsTest(base.BaseSharesAdminTest): expected_types, actual_types)) @test.attr(type=["gate", ]) - def test_create_cg_from_multi_typed_populated_cgsnapshot(self): + def test_create_cg_from_multi_typed_populated_cgsnapshot_v2_4(self): share_name = data_utils.rand_name("tempest-share-name") share_desc = data_utils.rand_name("tempest-share-description") share_size = 1 @@ -71,7 +77,9 @@ class ConsistencyGroupActionsTest(base.BaseSharesAdminTest): description=share_desc, size=share_size, consistency_group_id=self.consistency_group['id'], - share_type_id=self.share_type['id'] + share_type_id=self.share_type['id'], + client=self.shares_v2_client, + version='2.4', ) share_name2 = data_utils.rand_name("tempest-share-name") @@ -83,11 +91,16 @@ class ConsistencyGroupActionsTest(base.BaseSharesAdminTest): description=share_desc2, size=share_size2, consistency_group_id=self.consistency_group['id'], - share_type_id=self.share_type2['id'] + share_type_id=self.share_type2['id'], + client=self.shares_v2_client, + version='2.4', ) - cg_shares = self.shares_client.list_shares(detailed=True, params={ - 'consistency_group_id': self.consistency_group['id']}) + cg_shares = self.shares_v2_client.list_shares( + detailed=True, + params={'consistency_group_id': self.consistency_group['id']}, + version='2.4', + ) cg_share_ids = [s['id'] for s in cg_shares] for share_id in [share['id'], share2['id']]: @@ -101,10 +114,13 @@ class ConsistencyGroupActionsTest(base.BaseSharesAdminTest): self.consistency_group["id"], name=cgsnap_name, description=cgsnap_desc, - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4', + ) - self.create_consistency_group( - cleanup_in_class=False, source_cgsnapshot_id=cgsnapshot['id']) + self.create_consistency_group(cleanup_in_class=False, + source_cgsnapshot_id=cgsnapshot['id'], + version='2.4') # TODO(akerr): Skip until bug 1483886 is resolved # Verify that the new shares correspond to correct share types diff --git a/manila_tempest_tests/tests/api/admin/test_consistency_groups.py b/manila_tempest_tests/tests/api/admin/test_consistency_groups.py index 4fbe7f9a..d562e45f 100644 --- a/manila_tempest_tests/tests/api/admin/test_consistency_groups.py +++ b/manila_tempest_tests/tests/api/admin/test_consistency_groups.py @@ -43,11 +43,13 @@ class ConsistencyGroupsTest(base.BaseSharesAdminTest): cls.share_type2 = share_type['share_type'] @test.attr(type=["gate", ]) - def test_create_cg_with_multiple_share_types(self): + def test_create_cg_with_multiple_share_types_v2_4(self): # Create a consistency group consistency_group = self.create_consistency_group( - cleanup_in_class=False, share_type_ids=[self.share_type['id'], - self.share_type2['id']]) + cleanup_in_class=False, + share_type_ids=[self.share_type['id'], self.share_type2['id']], + version='2.4', + ) self.assertTrue(CG_REQUIRED_ELEMENTS.issubset( consistency_group.keys()), diff --git a/manila_tempest_tests/tests/api/admin/test_consistency_groups_negative.py b/manila_tempest_tests/tests/api/admin/test_consistency_groups_negative.py index a7a715f3..eafb5e82 100644 --- a/manila_tempest_tests/tests/api/admin/test_consistency_groups_negative.py +++ b/manila_tempest_tests/tests/api/admin/test_consistency_groups_negative.py @@ -51,6 +51,7 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesAdminTest): size=cls.share_size, consistency_group_id=cls.consistency_group['id'], share_type_id=cls.share_type['id'], + client=cls.shares_v2_client, ) # Create a cgsnapshot of the consistency group @@ -69,91 +70,105 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesAdminTest): self.share_type['id']) @test.attr(type=["negative", "gate", ]) - def test_create_share_of_unsupported_type_in_cg(self): + def test_create_share_of_unsupported_type_in_cg_v2_4(self): # Attempt to create share of default type in the cg self.assertRaises(exceptions.BadRequest, - self.shares_client.create_share, size=1, - consistency_group_id=self.consistency_group['id']) + self.create_share, + size=1, + consistency_group_id=self.consistency_group['id'], + client=self.shares_v2_client, + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_create_share_in_cg_that_is_not_available(self): + def test_create_share_in_cg_that_is_not_available_v2_4(self): consistency_group = self.create_consistency_group( - cleanup_in_class=False) - self.addCleanup(self.shares_client.consistency_group_reset_state, + cleanup_in_class=False, version='2.4') + self.addCleanup(self.shares_v2_client.consistency_group_reset_state, consistency_group['id'], - status='available') + status='available', + version='2.4') # creating - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='creating') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='creating', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'creating') self.assertRaises(exceptions.BadRequest, self.create_share, name=self.share_name, description=self.share_desc, size=self.share_size, consistency_group_id=consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + client=self.shares_v2_client, + version='2.4') # deleting - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='deleting') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='deleting', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'deleting') self.assertRaises(exceptions.BadRequest, self.create_share, name=self.share_name, description=self.share_desc, size=self.share_size, consistency_group_id=consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + client=self.shares_v2_client, + version='2.4') # error - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='error') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='error', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'error') self.assertRaises(exceptions.BadRequest, self.create_share, name=self.share_name, description=self.share_desc, size=self.share_size, consistency_group_id=consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + client=self.shares_v2_client, + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_create_cgsnapshot_of_cg_that_is_not_available(self): + def test_create_cgsnapshot_of_cg_that_is_not_available_v2_4(self): consistency_group = self.create_consistency_group( - cleanup_in_class=False) - self.addCleanup(self.shares_client.consistency_group_reset_state, + cleanup_in_class=False, version='2.4') + self.addCleanup(self.shares_v2_client.consistency_group_reset_state, consistency_group['id'], - status='available') + status='available', + version='2.4') # creating - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='creating') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='creating', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'creating') self.assertRaises(exceptions.Conflict, self.create_cgsnapshot_wait_for_active, consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') # deleting - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='deleting') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='deleting', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'deleting') self.assertRaises(exceptions.Conflict, self.create_cgsnapshot_wait_for_active, consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') # error - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='error') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='error', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'error') self.assertRaises(exceptions.Conflict, self.create_cgsnapshot_wait_for_active, consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_create_cgsnapshot_of_cg_with_share_in_error_state(self): - consistency_group = self.create_consistency_group() + def test_create_cgsnapshot_of_cg_with_share_in_error_state_v2_4(self): + consistency_group = self.create_consistency_group(version='2.4') share_name = data_utils.rand_name("tempest-share-name") share_desc = data_utils.rand_name("tempest-share-description") share_size = 1 @@ -163,65 +178,79 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesAdminTest): size=share_size, consistency_group_id=consistency_group['id'], cleanup_in_class=False, + client=self.shares_v2_client, + version='2.4', ) self.shares_client.reset_state(s_id=share['id']) self.shares_client.wait_for_share_status(share['id'], 'error') self.assertRaises(exceptions.Conflict, self.create_cgsnapshot_wait_for_active, consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_delete_cgsnapshot_not_in_available_or_error(self): + def test_delete_cgsnapshot_not_in_available_or_error_v2_4(self): cgsnapshot = self.create_cgsnapshot_wait_for_active( - self.consistency_group['id'], cleanup_in_class=False) - self.addCleanup(self.shares_client.cgsnapshot_reset_state, + self.consistency_group['id'], + cleanup_in_class=False, + version='2.4', + ) + self.addCleanup(self.shares_v2_client.cgsnapshot_reset_state, cgsnapshot['id'], - status='available') + status='available', + version='2.4') # creating - self.shares_client.cgsnapshot_reset_state(cgsnapshot['id'], - status='creating') - self.shares_client.wait_for_cgsnapshot_status(cgsnapshot['id'], - 'creating') + self.shares_v2_client.cgsnapshot_reset_state(cgsnapshot['id'], + status='creating', + version='2.4') + self.shares_v2_client.wait_for_cgsnapshot_status(cgsnapshot['id'], + 'creating') self.assertRaises(exceptions.Conflict, - self.shares_client.delete_cgsnapshot, - cgsnapshot['id']) + self.shares_v2_client.delete_cgsnapshot, + cgsnapshot['id'], + version='2.4') # deleting - self.shares_client.cgsnapshot_reset_state(cgsnapshot['id'], - status='deleting') - self.shares_client.wait_for_cgsnapshot_status(cgsnapshot['id'], - 'deleting') + self.shares_v2_client.cgsnapshot_reset_state(cgsnapshot['id'], + status='deleting', + version='2.4') + self.shares_v2_client.wait_for_cgsnapshot_status(cgsnapshot['id'], + 'deleting') self.assertRaises(exceptions.Conflict, - self.shares_client.delete_cgsnapshot, - cgsnapshot['id']) + self.shares_v2_client.delete_cgsnapshot, + cgsnapshot['id'], + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_delete_cg_not_in_available_or_error(self): + def test_delete_cg_not_in_available_or_error_v2_4(self): consistency_group = self.create_consistency_group( - cleanup_in_class=False) - self.addCleanup(self.shares_client.consistency_group_reset_state, + cleanup_in_class=False, version='2.4') + self.addCleanup(self.shares_v2_client.consistency_group_reset_state, consistency_group['id'], - status='available') + status='available', + version='2.4') # creating - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='creating') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='creating', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'creating') self.assertRaises(exceptions.Conflict, - self.shares_client.delete_consistency_group, - consistency_group['id']) + self.shares_v2_client.delete_consistency_group, + consistency_group['id'], + version='2.4') # deleting - self.shares_client.consistency_group_reset_state( - consistency_group['id'], status='deleting') - self.shares_client.wait_for_consistency_group_status( + self.shares_v2_client.consistency_group_reset_state( + consistency_group['id'], status='deleting', version='2.4') + self.shares_v2_client.wait_for_consistency_group_status( consistency_group['id'], 'deleting') self.assertRaises(exceptions.Conflict, - self.shares_client.delete_consistency_group, - consistency_group['id']) + self.shares_v2_client.delete_consistency_group, + consistency_group['id'], + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_create_cg_with_conflicting_share_types(self): + def test_create_cg_with_conflicting_share_types_v2_4(self): # Create conflicting share types name = data_utils.rand_name("tempest-manila") extra_specs = {"driver_handles_share_servers": False} @@ -237,10 +266,12 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesAdminTest): self.create_consistency_group, share_type_ids=[single_tenant_share_type['id'], multi_tenant_share_type['id']], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_create_cg_with_multi_tenant_share_type_and_no_share_network(self): + def test_create_cg_with_multi_tenant_share_type_and_no_share_network_v2_4( + self): # Create multi tenant share type name = data_utils.rand_name("tempest-manila") extra_specs = {"driver_handles_share_servers": True} @@ -248,12 +279,15 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesAdminTest): multi_tenant_share_type = share_type['share_type'] def create_cg(): - cg = self.shares_client.create_consistency_group( - share_type_ids=[multi_tenant_share_type['id']]) + cg = self.shares_v2_client.create_consistency_group( + share_type_ids=[multi_tenant_share_type['id']], + version='2.4' + ) resource = { "type": "consistency_group", "id": cg["id"], - "client": self.shares_client} + "client": self.shares_client + } self.method_resources.insert(0, resource) return cg @@ -262,9 +296,10 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesAdminTest): @test.attr(type=["negative", "gate", ]) def test_update_cg_share_types(self): consistency_group = self.create_consistency_group( - cleanup_in_class=False) + cleanup_in_class=False, version='2.4') self.assertRaises(exceptions.BadRequest, - self.shares_client.update_consistency_group, + self.shares_v2_client.update_consistency_group, consistency_group['id'], - share_types=[self.share_type['id']]) + share_types=[self.share_type['id']], + version='2.4') diff --git a/manila_tempest_tests/tests/api/admin/test_migration.py b/manila_tempest_tests/tests/api/admin/test_migration.py index 4c6bacfd..75b4ba76 100644 --- a/manila_tempest_tests/tests/api/admin/test_migration.py +++ b/manila_tempest_tests/tests/api/admin/test_migration.py @@ -37,7 +37,7 @@ class MigrationTest(base.BaseSharesAdminTest): raise cls.skipException(message) @test.attr(type=["gate", "smoke", ]) - def test_migration_empty(self): + def test_migration_empty_v2_5(self): if not CONF.share.migration_enabled: raise self.skipException("Migration tests disabled. Skipping.") @@ -60,7 +60,7 @@ class MigrationTest(base.BaseSharesAdminTest): old_export_location = share['export_locations'][0] - share = self.migrate_share(share['id'], dest_pool) + share = self.migrate_share(share['id'], dest_pool, version='2.5') self.assertEqual(dest_pool, share['host']) self.assertNotEqual(old_export_location, share['export_locations'][0]) diff --git a/manila_tempest_tests/tests/api/admin/test_share_instances.py b/manila_tempest_tests/tests/api/admin/test_share_instances.py new file mode 100644 index 00000000..1202b9d4 --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_share_instances.py @@ -0,0 +1,78 @@ +# Copyright 2015 Andrew Kerr +# 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 config +from tempest import test + +from manila_tempest_tests.tests.api import base + +CONF = config.CONF + + +class ShareInstancesTest(base.BaseSharesAdminTest): + + @classmethod + def resource_setup(cls): + super(ShareInstancesTest, cls).resource_setup() + cls.share = cls.create_share() + + @test.attr(type=["gate", ]) + def test_get_instances_of_share_v2_3(self): + """Test that we get only the 1 share instance back for the share.""" + share_instances = self.shares_v2_client.get_instances_of_share( + self.share['id'], version='2.3' + ) + + self.assertEqual(1, len(share_instances), + 'Too many share instances found; expected 1, ' + 'found %s' % len(share_instances)) + + si = share_instances[0] + self.assertEqual(self.share['id'], si['share_id'], + 'Share instance %s has incorrect share id value; ' + 'expected %s, got %s.' % (si['id'], + self.share['id'], + si['share_id'])) + + @test.attr(type=["gate", ]) + def test_list_share_instances_v2_3(self): + """Test that we get at least the share instance back for the share.""" + share_instances = self.shares_v2_client.get_instances_of_share( + self.share['id'], version='2.3' + ) + + share_ids = [si['share_id'] for si in share_instances] + + msg = 'Share instance for share %s was not found.' % self.share['id'] + self.assertIn(self.share['id'], share_ids, msg) + + @test.attr(type=["gate", ]) + def test_get_share_instance_v2_3(self): + """Test that we get the proper keys back for the instance.""" + share_instances = self.shares_v2_client.get_instances_of_share( + self.share['id'], version='2.3' + ) + si = self.shares_v2_client.get_share_instance(share_instances[0]['id'], + version='2.3') + + expected_keys = ['host', 'share_id', 'id', 'share_network_id', + 'status', 'availability_zone', 'share_server_id', + 'export_locations', 'export_location', 'created_at'] + actual_keys = si.keys() + self.assertEqual(sorted(expected_keys), sorted(actual_keys), + 'Share instance %s returned incorrect keys; ' + 'expected %s, got %s.' % (si['id'], + sorted(expected_keys), + sorted(actual_keys))) diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py index 65e299da..591347e4 100644 --- a/manila_tempest_tests/tests/api/base.py +++ b/manila_tempest_tests/tests/api/base.py @@ -98,7 +98,8 @@ class BaseSharesTest(test.BaseTestCase): def get_client_with_isolated_creds(cls, name=None, type_of_creds="admin", - cleanup_in_class=False): + cleanup_in_class=False, + client_version='1'): """Creates isolated creds. :param name: name, will be used for naming ic and related stuff @@ -126,7 +127,10 @@ class BaseSharesTest(test.BaseTestCase): # create client with isolated creds os = clients.Manager(credentials=creds) - client = os.shares_client + if client_version == '1': + client = os.shares_client + elif client_version == '2': + client = os.shares_v2_client # Set place where will be deleted isolated creds ic_res = { @@ -183,7 +187,9 @@ class BaseSharesTest(test.BaseTestCase): nc = cls.os.network_client share_network_id = cls.provide_share_network(sc, nc) cls.os.shares_client.share_network_id = share_network_id + cls.os.shares_v2_client.share_network_id = share_network_id cls.shares_client = cls.os.shares_client + cls.shares_v2_client = cls.os.shares_v2_client def setUp(self): super(BaseSharesTest, self).setUp() @@ -281,12 +287,12 @@ class BaseSharesTest(test.BaseTestCase): snapshot_id=None, description=None, metadata=None, share_network_id=None, share_type_id=None, consistency_group_id=None, client=None, - cleanup_in_class=True, is_public=False): + cleanup_in_class=True, is_public=False, **kwargs): client = client or cls.shares_client description = description or "Tempest's share" share_network_id = share_network_id or client.share_network_id or None metadata = metadata or {} - kwargs = { + kwargs.update({ 'share_protocol': share_protocol, 'size': size, 'name': name, @@ -296,7 +302,7 @@ class BaseSharesTest(test.BaseTestCase): 'share_network_id': share_network_id, 'share_type_id': share_type_id, 'is_public': is_public, - } + }) if consistency_group_id: kwargs['consistency_group_id'] = consistency_group_id @@ -309,9 +315,9 @@ class BaseSharesTest(test.BaseTestCase): return share @classmethod - def migrate_share(cls, share_id, dest_host, client=None): - client = client or cls.shares_client - client.migrate_share(share_id, dest_host) + def migrate_share(cls, share_id, dest_host, client=None, **kwargs): + client = client or cls.shares_v2_client + client.migrate_share(share_id, dest_host, **kwargs) share = client.wait_for_migration_completed(share_id, dest_host) return share @@ -383,7 +389,7 @@ class BaseSharesTest(test.BaseTestCase): @classmethod def create_consistency_group(cls, client=None, cleanup_in_class=True, share_network_id=None, **kwargs): - client = client or cls.shares_client + client = client or cls.shares_v2_client kwargs['share_network_id'] = (share_network_id or client.share_network_id or None) consistency_group = client.create_consistency_group(**kwargs) @@ -440,12 +446,15 @@ class BaseSharesTest(test.BaseTestCase): @classmethod def create_cgsnapshot_wait_for_active(cls, consistency_group_id, name=None, description=None, - client=None, cleanup_in_class=True): - client = client or cls.shares_client + client=None, cleanup_in_class=True, + **kwargs): + client = client or cls.shares_v2_client if description is None: description = "Tempest's cgsnapshot" - cgsnapshot = client.create_cgsnapshot(consistency_group_id, name=name, - description=description) + cgsnapshot = client.create_cgsnapshot(consistency_group_id, + name=name, + description=description, + **kwargs) resource = { "type": "cgsnapshot", "id": cgsnapshot["id"], @@ -556,11 +565,12 @@ class BaseSharesTest(test.BaseTestCase): client = res["client"] with handle_cleanup_exceptions(): if res["type"] is "share": - params = None cg_id = res.get('consistency_group_id') if cg_id: params = {'consistency_group_id': cg_id} - client.delete_share(res_id, params=params) + client.delete_share(res_id, params=params) + else: + client.delete_share(res_id) client.wait_for_resource_deletion(share_id=res_id) elif res["type"] is "snapshot": client.delete_snapshot(res_id) @@ -679,6 +689,7 @@ class BaseSharesAltTest(BaseSharesTest): cls.os = clients.AltManager() alt_share_network_id = CONF.share.alt_share_network_id cls.os.shares_client.share_network_id = alt_share_network_id + cls.os.shares_v2_client.share_network_id = alt_share_network_id super(BaseSharesAltTest, cls).resource_setup() @@ -694,4 +705,5 @@ class BaseSharesAdminTest(BaseSharesTest): cls.os = clients.AdminManager() admin_share_network_id = CONF.share.admin_share_network_id cls.os.shares_client.share_network_id = admin_share_network_id + cls.os.shares_v2_client.share_network_id = admin_share_network_id super(BaseSharesAdminTest, cls).resource_setup() diff --git a/manila_tempest_tests/tests/api/test_consistency_group_actions.py b/manila_tempest_tests/tests/api/test_consistency_group_actions.py index 81bff8ac..0bf6e58e 100644 --- a/manila_tempest_tests/tests/api/test_consistency_group_actions.py +++ b/manila_tempest_tests/tests/api/test_consistency_group_actions.py @@ -57,6 +57,7 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): size=cls.share_size, consistency_group_id=cls.consistency_group['id'], metadata={'key': 'value'}, + client=cls.shares_v2_client, ) cls.share_name2 = data_utils.rand_name("tempest-share-name") @@ -67,6 +68,7 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): description=cls.share_desc2, size=cls.share_size2, consistency_group_id=cls.consistency_group['id'], + client=cls.shares_v2_client, ) cls.cgsnap_name = data_utils.rand_name("tempest-cgsnap-name") @@ -93,6 +95,7 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): description=cls.share_desc3, size=cls.share_size, consistency_group_id=cls.consistency_group2['id'], + client=cls.shares_v2_client, ) cls.cgsnap_name2 = data_utils.rand_name("tempest-cgsnap-name") @@ -103,11 +106,11 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): description=cls.cgsnap_desc2) @test.attr(type=["gate", ]) - def test_get_consistency_group(self): + def test_get_consistency_group_v2_4(self): # Get consistency group - consistency_group = self.shares_client.get_consistency_group( - self.consistency_group['id']) + consistency_group = self.shares_v2_client.get_consistency_group( + self.consistency_group['id'], version='2.4') # Verify keys actual_keys = set(consistency_group.keys()) @@ -129,10 +132,11 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): msg) @test.attr(type=["gate", ]) - def test_get_share(self): + def test_get_share_v2_4(self): # Get share - share = self.shares_client.get_share(self.share['id']) + share = self.shares_v2_client.get_share(self.share['id'], + version='2.4') # Verify keys expected_keys = {"status", "description", "links", "availability_zone", @@ -165,10 +169,11 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): self.consistency_group["id"], share["consistency_group_id"], msg) @test.attr(type=["gate", ]) - def test_list_consistency_groups(self): + def test_list_consistency_groups_v2_4(self): # List consistency groups - consistency_groups = self.shares_client.list_consistency_groups() + consistency_groups = self.shares_v2_client.list_consistency_groups( + version='2.4') # Verify keys [self.assertEqual(CG_SIMPLE_KEYS, set(cg.keys())) for cg in @@ -184,11 +189,11 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): self.assertEqual(1, len(gen), msg) @test.attr(type=["gate", ]) - def test_list_consistency_groups_with_detail(self): + def test_list_consistency_groups_with_detail_v2_4(self): # List consistency groups - consistency_groups = self.shares_client.list_consistency_groups( - detailed=True) + consistency_groups = self.shares_v2_client.list_consistency_groups( + detailed=True, version='2.4') # Verify keys [self.assertTrue(CG_DETAIL_REQUIRED_KEYS.issubset(set(cg.keys()))) @@ -204,10 +209,13 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): self.assertEqual(1, len(gen), msg) @test.attr(type=["gate", ]) - def test_filter_shares_by_consistency_group_id(self): + def test_filter_shares_by_consistency_group_id_v2_4(self): - shares = self.shares_client.list_shares(detailed=True, params={ - 'consistency_group_id': self.consistency_group['id']}) + shares = self.shares_v2_client.list_shares( + detailed=True, + params={'consistency_group_id': self.consistency_group['id']}, + version='2.4' + ) share_ids = [share['id'] for share in shares] @@ -222,10 +230,10 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): % (self.share['id'], share_ids)) @test.attr(type=["gate", ]) - def test_get_cgsnapshot(self): + def test_get_cgsnapshot_v2_4(self): # Get consistency group - consistency_group = self.shares_client.get_consistency_group( - self.consistency_group['id']) + consistency_group = self.shares_v2_client.get_consistency_group( + self.consistency_group['id'], version='2.4') # Verify keys actual_keys = set(consistency_group.keys()) @@ -247,10 +255,10 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): msg) @test.attr(type=["gate", ]) - def test_get_cgsnapshot_members(self): + def test_get_cgsnapshot_members_v2_4(self): - cgsnapshot_members = self.shares_client.list_cgsnapshot_members( - self.cgsnapshot['id']) + cgsnapshot_members = self.shares_v2_client.list_cgsnapshot_members( + self.cgsnapshot['id'], version='2.4') member_share_ids = [member['share_id'] for member in cgsnapshot_members] self.assertEqual(2, len(cgsnapshot_members), @@ -272,17 +280,22 @@ class ConsistencyGroupActionsTest(base.BaseSharesTest): # member['share_type_id']) @test.attr(type=["gate", "smoke", ]) - def test_create_consistency_group_from_populated_cgsnapshot(self): + def test_create_consistency_group_from_populated_cgsnapshot_v2_4(self): - cgsnapshot_members = self.shares_client.list_cgsnapshot_members( - self.cgsnapshot['id']) + cgsnapshot_members = self.shares_v2_client.list_cgsnapshot_members( + self.cgsnapshot['id'], version='2.4') new_consistency_group = self.create_consistency_group( - cleanup_in_class=False, source_cgsnapshot_id=self.cgsnapshot['id']) + cleanup_in_class=False, + source_cgsnapshot_id=self.cgsnapshot['id'], + version='2.4' + ) - new_shares = self.shares_client.list_shares( + new_shares = self.shares_v2_client.list_shares( params={'consistency_group_id': new_consistency_group['id']}, - detailed=True) + detailed=True, + version='2.4' + ) # Verify each new share is available for share in new_shares: @@ -326,46 +339,58 @@ class ConsistencyGroupRenameTest(base.BaseSharesTest): ) @test.attr(type=["gate", ]) - def test_update_consistency_group(self): + def test_update_consistency_group_v2_4(self): # Get consistency_group - consistency_group = self.shares_client.get_consistency_group( - self.consistency_group['id']) + consistency_group = self.shares_v2_client.get_consistency_group( + self.consistency_group['id'], version='2.4') self.assertEqual(self.cg_name, consistency_group["name"]) self.assertEqual(self.cg_desc, consistency_group["description"]) # Update consistency_group new_name = data_utils.rand_name("tempest-new-name") new_desc = data_utils.rand_name("tempest-new-description") - updated = self.shares_client.update_consistency_group( - consistency_group["id"], name=new_name, description=new_desc) + updated = self.shares_v2_client.update_consistency_group( + consistency_group["id"], + name=new_name, + description=new_desc, + version='2.4' + ) self.assertEqual(new_name, updated["name"]) self.assertEqual(new_desc, updated["description"]) # Get consistency_group - consistency_group = self.shares_client.get_consistency_group( - self.consistency_group['id']) + consistency_group = self.shares_v2_client.get_consistency_group( + self.consistency_group['id'], version='2.4') self.assertEqual(new_name, consistency_group["name"]) self.assertEqual(new_desc, consistency_group["description"]) @test.attr(type=["gate", ]) - def test_create_update_read_consistency_group_with_unicode(self): + def test_create_update_read_consistency_group_with_unicode_v2_4(self): value1 = u'ಠ_ಠ' value2 = u'ಠ_ರೃ' # Create consistency_group consistency_group = self.create_consistency_group( - cleanup_in_class=False, name=value1, description=value1) + cleanup_in_class=False, + name=value1, + description=value1, + version='2.4' + ) self.assertEqual(value1, consistency_group["name"]) self.assertEqual(value1, consistency_group["description"]) # Update consistency_group - updated = self.shares_client.update_consistency_group( - consistency_group["id"], name=value2, description=value2) + updated = self.shares_v2_client.update_consistency_group( + consistency_group["id"], + name=value2, + description=value2, + version='2.4' + ) self.assertEqual(value2, updated["name"]) self.assertEqual(value2, updated["description"]) # Get consistency_group - consistency_group = self.shares_client.get_consistency_group( - consistency_group['id']) + consistency_group = self.shares_v2_client.get_consistency_group( + consistency_group['id'], version='2.4') self.assertEqual(value2, consistency_group["name"]) self.assertEqual(value2, consistency_group["description"]) diff --git a/manila_tempest_tests/tests/api/test_consistency_groups.py b/manila_tempest_tests/tests/api/test_consistency_groups.py index 1ba3902b..33ea3347 100644 --- a/manila_tempest_tests/tests/api/test_consistency_groups.py +++ b/manila_tempest_tests/tests/api/test_consistency_groups.py @@ -33,10 +33,10 @@ class ConsistencyGroupsTest(base.BaseSharesTest): """Covers consistency group functionality.""" @test.attr(type=["gate", ]) - def test_create_populate_delete_consistency_group(self): + def test_create_populate_delete_consistency_group_v2_4(self): # Create a consistency group consistency_group = self.create_consistency_group( - cleanup_in_class=False) + cleanup_in_class=False, version='2.4') self.assertTrue(CG_REQUIRED_ELEMENTS.issubset( consistency_group.keys()), 'At least one expected element missing from consistency group ' @@ -45,31 +45,35 @@ class ConsistencyGroupsTest(base.BaseSharesTest): "actual": consistency_group.keys()}) # Populate share = self.create_share(consistency_group_id=consistency_group['id'], - cleanup_in_class=False) + cleanup_in_class=False, + client=self.shares_v2_client, + version='2.4') # Delete params = {"consistency_group_id": consistency_group['id']} - self.shares_client.delete_share(share['id'], params=params) + self.shares_v2_client.delete_share(share['id'], params=params, + version='2.4') self.shares_client.wait_for_resource_deletion(share_id=share['id']) - self.shares_client.delete_consistency_group(consistency_group['id']) - self.shares_client.wait_for_resource_deletion( + self.shares_v2_client.delete_consistency_group(consistency_group['id'], + version='2.4') + self.shares_v2_client.wait_for_resource_deletion( cg_id=consistency_group['id']) # Verify self.assertRaises(lib_exc.NotFound, - self.shares_client.get_consistency_group, + self.shares_v2_client.get_consistency_group, consistency_group['id']) self.assertRaises(lib_exc.NotFound, self.shares_client.get_share, share['id']) @test.attr(type=["gate", ]) - def test_create_delete_empty_cgsnapshot(self): + def test_create_delete_empty_cgsnapshot_v2_4(self): # Create base consistency group consistency_group = self.create_consistency_group( - cleanup_in_class=False) + cleanup_in_class=False, version='2.4') # Create cgsnapshot cgsnapshot = self.create_cgsnapshot_wait_for_active( - consistency_group["id"], cleanup_in_class=False) + consistency_group["id"], cleanup_in_class=False, version='2.4') self.assertTrue(CGSNAPSHOT_REQUIRED_ELEMENTS.issubset( cgsnapshot.keys()), @@ -78,19 +82,22 @@ class ConsistencyGroupsTest(base.BaseSharesTest): "expected": CGSNAPSHOT_REQUIRED_ELEMENTS, "actual": cgsnapshot.keys()}) - cgsnapshot_members = self.shares_client.list_cgsnapshot_members( - cgsnapshot['id']) + cgsnapshot_members = self.shares_v2_client.list_cgsnapshot_members( + cgsnapshot['id'], version='2.4') self.assertEmpty(cgsnapshot_members, 'Expected 0 cgsnapshot members, got %s' % len( cgsnapshot_members)) # delete snapshot - self.shares_client.delete_cgsnapshot(cgsnapshot["id"]) - self.shares_client.wait_for_resource_deletion( + self.shares_v2_client.delete_cgsnapshot(cgsnapshot["id"], + version='2.4') + self.shares_v2_client.wait_for_resource_deletion( cgsnapshot_id=cgsnapshot["id"]) self.assertRaises(lib_exc.NotFound, - self.shares_client.get_cgsnapshot, cgsnapshot['id']) + self.shares_v2_client.get_cgsnapshot, + cgsnapshot['id'], + version='2.4') @test.attr(type=["gate", "smoke", ]) def test_create_consistency_group_from_empty_cgsnapshot(self): @@ -102,7 +109,7 @@ class ConsistencyGroupsTest(base.BaseSharesTest): cgsnapshot = self.create_cgsnapshot_wait_for_active( consistency_group["id"], cleanup_in_class=False) - cgsnapshot_members = self.shares_client.list_cgsnapshot_members( + cgsnapshot_members = self.shares_v2_client.list_cgsnapshot_members( cgsnapshot['id']) self.assertEmpty(cgsnapshot_members, @@ -123,8 +130,8 @@ class ConsistencyGroupsTest(base.BaseSharesTest): self.assertEqual(new_consistency_group['source_cgsnapshot_id'], cgsnapshot['id'], msg) - msg = 'Unexpected share_types on new consistency group. Expected %s, ' \ - 'got %s.' % (consistency_group['share_types'], - new_consistency_group['share_types']) + msg = ('Unexpected share_types on new consistency group. Expected ' + '%s, got %s.' % (consistency_group['share_types'], + new_consistency_group['share_types'])) self.assertEqual(sorted(consistency_group['share_types']), sorted(new_consistency_group['share_types']), msg) diff --git a/manila_tempest_tests/tests/api/test_consistency_groups_negative.py b/manila_tempest_tests/tests/api/test_consistency_groups_negative.py index 7c813c0e..34c0a6dd 100644 --- a/manila_tempest_tests/tests/api/test_consistency_groups_negative.py +++ b/manila_tempest_tests/tests/api/test_consistency_groups_negative.py @@ -46,7 +46,8 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesTest): name=cls.share_name, description=cls.share_desc, size=cls.share_size, - consistency_group_id=cls.consistency_group['id'] + consistency_group_id=cls.consistency_group['id'], + client=cls.shares_v2_client ) # Create a cgsnapshot of the consistency group cls.cgsnap_name = data_utils.rand_name("tempest-cgsnap-name") @@ -57,149 +58,174 @@ class ConsistencyGroupsNegativeTest(base.BaseSharesTest): description=cls.cgsnap_desc) @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cg_with_invalid_source_cgsnapshot_id_value( + def test_create_cg_with_invalid_source_cgsnapshot_id_value_v2_4( self): self.assertRaises(lib_exc.BadRequest, self.create_consistency_group, source_cgsnapshot_id='foobar', - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cg_with_nonexistent_source_cgsnapshot_id_value(self): + def test_create_cg_with_nonexistent_source_cgsnapshot_id_value_v2_4(self): self.assertRaises(lib_exc.BadRequest, self.create_consistency_group, source_cgsnapshot_id=self.share['id'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cg_with_invalid_share_network_id_value( - self): + def test_create_cg_with_invalid_share_network_id_value_v2_4(self): self.assertRaises(lib_exc.BadRequest, self.create_consistency_group, share_network_id='foobar', - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cg_with_nonexistent_share_network_id_value(self): + def test_create_cg_with_nonexistent_share_network_id_value_v2_4(self): self.assertRaises(lib_exc.BadRequest, self.create_consistency_group, share_network_id=self.share['id'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cg_with_invalid_share_type_id_value( - self): + def test_create_cg_with_invalid_share_type_id_value_v2_4(self): self.assertRaises(lib_exc.BadRequest, self.create_consistency_group, share_type_ids=['foobar'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cg_with_nonexistent_share_type_id_value(self): + def test_create_cg_with_nonexistent_share_type_id_value_v2_4(self): self.assertRaises(lib_exc.BadRequest, self.create_consistency_group, share_type_ids=[self.share['id']], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cgsnapshot_with_invalid_cg_id_value( - self): + def test_create_cgsnapshot_with_invalid_cg_id_value_v2_4(self): self.assertRaises(lib_exc.BadRequest, self.create_cgsnapshot_wait_for_active, 'foobar', - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_create_cgsnapshot_with_nonexistent_cg_id_value(self): + def test_create_cgsnapshot_with_nonexistent_cg_id_value_v2_4(self): self.assertRaises(lib_exc.BadRequest, self.create_cgsnapshot_wait_for_active, self.share['id'], - cleanup_in_class=False) + cleanup_in_class=False, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_get_cg_with_wrong_id(self): + def test_get_cg_with_wrong_id_v2_4(self): self.assertRaises(lib_exc.NotFound, - self.shares_client.get_consistency_group, - "wrong_consistency_group_id") + self.shares_v2_client.get_consistency_group, + "wrong_consistency_group_id", + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_get_cg_without_passing_cg_id(self): + def test_get_cg_without_passing_cg_id_v2_4(self): self.assertRaises(lib_exc.NotFound, - self.shares_client.get_consistency_group, '') + self.shares_v2_client.get_consistency_group, + '', + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_update_cg_with_wrong_id(self): + def test_update_cg_with_wrong_id_v2_4(self): self.assertRaises(lib_exc.NotFound, - self.shares_client.update_consistency_group, + self.shares_v2_client.update_consistency_group, 'wrong_consistency_group_id', name='new_name', - description='new_description') + description='new_description', + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_delete_cg_with_wrong_id(self): + def test_delete_cg_with_wrong_id_v2_4(self): self.assertRaises(lib_exc.NotFound, - self.shares_client.delete_consistency_group, - "wrong_consistency_group_id") + self.shares_v2_client.delete_consistency_group, + "wrong_consistency_group_id", + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_delete_cg_without_passing_cg_id(self): + def test_delete_cg_without_passing_cg_id_v2_4(self): self.assertRaises(lib_exc.NotFound, - self.shares_client.delete_consistency_group, '') + self.shares_v2_client.delete_consistency_group, + '', + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_delete_cg_in_use_by_cgsnapshot(self): + def test_delete_cg_in_use_by_cgsnapshot_v2_4(self): # Attempt delete of share type self.assertRaises(lib_exc.Conflict, - self.shares_client.delete_consistency_group, - self.consistency_group['id']) + self.shares_v2_client.delete_consistency_group, + self.consistency_group['id'], + version='2.4') @test.attr(type=["negative", "gate", ]) - def test_delete_share_in_use_by_cgsnapshot(self): + def test_delete_share_in_use_by_cgsnapshot_v2_4(self): # Attempt delete of share type params = {'consistency_group_id': self.share['consistency_group_id']} self.assertRaises(lib_exc.Forbidden, - self.shares_client.delete_share, + self.shares_v2_client.delete_share, self.share['id'], - params=params) + params=params, + version='2.4') @test.attr(type=["negative", "smoke", "gate", ]) - def test_delete_cg_containing_a_share(self): + def test_delete_cg_containing_a_share_v2_4(self): self.assertRaises(lib_exc.Conflict, - self.shares_client.delete_consistency_group, - self.consistency_group['id']) + self.shares_v2_client.delete_consistency_group, + self.consistency_group['id'], + version='2.4') # Verify consistency group is not put into error state from conflict - cg = self.shares_client.get_consistency_group( - self.consistency_group['id']) + cg = self.shares_v2_client.get_consistency_group( + self.consistency_group['id'], version='2.4') self.assertEqual('available', cg['status']) @test.attr(type=["negative", "smoke", "gate", ]) - def test_filter_shares_on_invalid_cg_id(self): - shares = self.shares_client.list_shares(detailed=True, params={ - 'consistency_group_id': 'foobar'}) + def test_filter_shares_on_invalid_cg_id_v2_4(self): + shares = self.shares_v2_client.list_shares( + detailed=True, + params={'consistency_group_id': 'foobar'}, + version='2.4' + ) - self.assertEqual(0, len(shares), 'Incorrect number of shares ' - 'returned. Expected 0, got %s.' % - len(shares)) + self.assertEqual(0, len(shares), + 'Incorrect number of shares returned. Expected 0, ' + 'got %s.' % len(shares)) @test.attr(type=["negative", "smoke", "gate", ]) - def test_filter_shares_on_nonexistent_cg_id(self): - shares = self.shares_client.list_shares(detailed=True, params={ - 'consistency_group_id': self.share['id']}) + def test_filter_shares_on_nonexistent_cg_id_v2_4(self): + shares = self.shares_v2_client.list_shares( + detailed=True, + params={'consistency_group_id': self.share['id']}, + version='2.4' + ) - self.assertEqual(0, len(shares), 'Incorrect number of shares ' - 'returned. Expected 0, got %s.' % - len(shares)) + self.assertEqual(0, len(shares), + 'Incorrect number of shares returned. Expected 0, ' + 'got %s.' % len(shares)) @test.attr(type=["negative", "smoke", "gate", ]) - def test_filter_shares_on_empty_cg_id(self): + def test_filter_shares_on_empty_cg_id_v2_4(self): consistency_group = self.create_consistency_group( name='tempest_cg', description='tempest_cg_desc', cleanup_in_class=False, + version='2.4', + ) + shares = self.shares_v2_client.list_shares( + detailed=True, + params={'consistency_group_id': consistency_group['id']}, + version='2.4', ) - shares = self.shares_client.list_shares(detailed=True, params={ - 'consistency_group_id': consistency_group['id']}) - self.assertEqual(0, len(shares), 'Incorrect number of shares ' - 'returned. Expected 0, got %s.' % - len(shares)) + self.assertEqual(0, len(shares), + 'Incorrect number of shares returned. Expected 0, ' + 'got %s.' % len(shares)) diff --git a/manila_tempest_tests/tests/api/test_microversions.py b/manila_tempest_tests/tests/api/test_microversions.py index 2984b04f..1de8b59b 100644 --- a/manila_tempest_tests/tests/api/test_microversions.py +++ b/manila_tempest_tests/tests/api/test_microversions.py @@ -1,4 +1,5 @@ # Copyright 2015 Goutham Pacha Ravi +# Copyright 2015 Clinton Knight # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -20,90 +21,152 @@ from manila_tempest_tests.tests.api import base CONF = config.CONF +API_MICROVERSIONS_HEADER_LOWER = 'x-openstack-manila-api-version' +API_MICROVERSIONS_HEADER = 'X-OpenStack-Manila-API-Version' +_MIN_API_VERSION = CONF.share.min_api_microversion +_MAX_API_VERSION = CONF.share.max_api_microversion + class MicroversionsTest(base.BaseSharesTest): """Request and validate REST API Microversions. - Sends a HTTP GET request with the base endpoint to request a Microversion. + Sends HTTP GET requests to the version API to validate microversions. """ - _MIN_API_VERSION = CONF.share.min_api_microversion - _MAX_API_VERSION = CONF.share.max_api_microversion + @test.attr(type=["gate", "smoke", ]) + def test_microversions_root_version(self): + + resp, resp_body = self.shares_v2_client.send_microversion_request() + + self.assertEqual(300, resp.status) + + version_list = resp_body['versions'] + ids = [v['id'] for v in version_list] + self.assertEqual({'v1.0', 'v2.0'}, set(ids)) + + self.assertNotIn(API_MICROVERSIONS_HEADER_LOWER, resp) + self.assertNotIn('vary', resp) + + v1 = [v for v in version_list if v['id'] == 'v1.0'][0] + self.assertEqual('', v1.get('min_version')) + self.assertEqual('', v1.get('version')) + + v2 = [v for v in version_list if v['id'] == 'v2.0'][0] + self.assertEqual(_MIN_API_VERSION, v2.get('min_version')) + self.assertEqual(_MAX_API_VERSION, v2.get('version')) @test.attr(type=["gate", "smoke", ]) - def test_microversions_no_version(self): - resp, resp_body = self.shares_client.send_microversion_request() + def test_microversions_v1_no_version(self): - self.assertEqual(self._MIN_API_VERSION, - resp[self.shares_client.API_MICROVERSIONS_HEADER]) - self.assertTrue(len(resp_body['versions']) > 0) - self.assertNotIn('min_version', resp_body['versions'][0]) - self.assertNotIn('version', resp_body['versions'][0]) + resp, resp_body = self.shares_v2_client.send_microversion_request( + script_name='v1') + + self.assertEqual(200, resp.status) + + version_list = resp_body['versions'] + ids = [v['id'] for v in version_list] + self.assertEqual({'v1.0'}, set(ids)) + + self.assertEqual('1.0', resp.get(API_MICROVERSIONS_HEADER_LOWER)) + self.assertEqual(API_MICROVERSIONS_HEADER, resp.get('vary')) + self.assertEqual('', version_list[0].get('min_version')) + self.assertEqual('', version_list[0].get('version')) @test.attr(type=["gate", "smoke", ]) - def test_microversions_version_min_version(self): - """Requests base version 1.0.""" + def test_microversions_v1_with_version(self): - resp, resp_body = self.shares_client.send_microversion_request( - self._MIN_API_VERSION) + resp, resp_body = self.shares_v2_client.send_microversion_request( + script_name='v1', version='5.0') - self.assertEqual(self._MIN_API_VERSION, - resp[self.shares_client.API_MICROVERSIONS_HEADER]) - self.assertTrue(len(resp_body['versions']) > 0) - self.assertNotIn('min_version', resp_body['versions'][0]) - self.assertNotIn('version', resp_body['versions'][0]) + self.assertEqual(200, resp.status) + + version_list = resp_body['versions'] + ids = [v['id'] for v in version_list] + self.assertEqual({'v1.0'}, set(ids)) + + self.assertEqual('1.0', resp.get(API_MICROVERSIONS_HEADER_LOWER)) + self.assertEqual(API_MICROVERSIONS_HEADER, resp.get('vary')) + self.assertEqual('', version_list[0].get('min_version')) + self.assertEqual('', version_list[0].get('version')) @test.attr(type=["gate", "smoke", ]) - def test_microversions_version_max_configured_version(self): - """Requests maximum API microversion. + def test_microversions_v2_no_version(self): - Requests the current maximum API microversion from the Manila API - service, and confirms that version is the same as what Tempest is - configured to request in other versioned API calls. - """ + resp, resp_body = self.shares_v2_client.send_microversion_request( + script_name='v2') - resp, resp_body = self.shares_client.send_microversion_request( - self._MAX_API_VERSION) + self.assertEqual(200, resp.status) - self.assertEqual(self._MAX_API_VERSION, - resp[self.shares_client.API_MICROVERSIONS_HEADER]) - self.assertTrue(len(resp_body['versions']) > 0) - self.assertEqual(self._MAX_API_VERSION, - resp_body['versions'][0]['version']) + version_list = resp_body['versions'] + ids = [v['id'] for v in version_list] + self.assertEqual({'v2.0'}, set(ids)) + + self.assertEqual(_MIN_API_VERSION, + resp.get(API_MICROVERSIONS_HEADER_LOWER)) + self.assertEqual(API_MICROVERSIONS_HEADER, resp.get('vary')) + self.assertEqual(_MIN_API_VERSION, version_list[0].get('min_version')) + self.assertEqual(_MAX_API_VERSION, version_list[0].get('version')) @test.attr(type=["gate", "smoke", ]) - def test_microversions_version_1_1(self): - """Requests version 1.1, the first Manila microversion.""" + def test_microversions_v2_min_version(self): - resp, resp_body = self.shares_client.send_microversion_request('1.1') + resp, resp_body = self.shares_v2_client.send_microversion_request( + script_name='v2', version=_MIN_API_VERSION) - self.assertEqual('1.1', - resp[self.shares_client.API_MICROVERSIONS_HEADER]) - self.assertTrue(len(resp_body['versions']) > 0) - self.assertEqual(self._MIN_API_VERSION, - resp_body['versions'][0]['min_version']) + self.assertEqual(200, resp.status) + + version_list = resp_body['versions'] + ids = [v['id'] for v in version_list] + self.assertEqual({'v2.0'}, set(ids)) + + self.assertEqual(_MIN_API_VERSION, + resp.get(API_MICROVERSIONS_HEADER_LOWER)) + self.assertEqual(API_MICROVERSIONS_HEADER, resp.get('vary')) + self.assertEqual(_MIN_API_VERSION, version_list[0].get('min_version')) + self.assertEqual(_MAX_API_VERSION, version_list[0].get('version')) @test.attr(type=["gate", "smoke", ]) - def test_microversions_unavailable_versions(self): - """Requests a version greater than the latest available version.""" + def test_microversions_v2_max_version(self): - resp, resp_body = self.shares_client.send_microversion_request('1.1') - self.assertTrue(len(resp_body['versions']) > 0) - major_ver, minor_ver = [int(ver) for ver in - resp_body['versions'][0]['version'].split(".")] - req_version = ('%s.%s' % (major_ver + 1, minor_ver + 1)) - resp, _ = self.shares_client.send_microversion_request(req_version) + resp, resp_body = self.shares_v2_client.send_microversion_request( + script_name='v2', version=_MAX_API_VERSION) + + self.assertEqual(200, resp.status) + + version_list = resp_body['versions'] + ids = [v['id'] for v in version_list] + self.assertEqual({'v2.0'}, set(ids)) + + self.assertEqual(_MAX_API_VERSION, + resp.get(API_MICROVERSIONS_HEADER_LOWER)) + self.assertEqual(API_MICROVERSIONS_HEADER, resp.get('vary')) + self.assertEqual(_MIN_API_VERSION, version_list[0].get('min_version')) + self.assertEqual(_MAX_API_VERSION, version_list[0].get('version')) + + @test.attr(type=["gate", "smoke", ]) + def test_microversions_v2_invalid_version(self): + + resp, _ = self.shares_v2_client.send_microversion_request( + script_name='v2', version='1.2.1') + + self.assertEqual(400, resp.status) + + @test.attr(type=["gate", "smoke", ]) + def test_microversions_v2_unacceptable_version(self): + + # First get max version from the server + resp, resp_body = self.shares_v2_client.send_microversion_request( + script_name='v2') + + self.assertEqual(200, resp.status) + + version_list = resp_body['versions'] + latest_version = version_list[0].get('version') + major, minor = [int(ver) for ver in latest_version.split(".")] + next_version = ('%s.%s' % (major + 1, minor + 1)) + + # Request a version that is too high + resp, _ = self.shares_v2_client.send_microversion_request( + script_name='v2', version=next_version) self.assertEqual(406, resp.status) - - @test.attr(type=["gate", "smoke", ]) - def test_microversions_invalid_versions(self): - """Requests invalid versions.""" - - resp, resp_body = self.shares_client.send_microversion_request('1.2.1') - - self.assertEqual(400, resp.status) - - resp, _ = self.shares_client.send_microversion_request('None') - - self.assertEqual(400, resp.status)