summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-01-21 23:56:39 +0000
committerGerrit Code Review <review@openstack.org>2016-01-21 23:56:39 +0000
commitea8276cd1068a972ad96c3760e2a86e485ae086c (patch)
tree2987688772caf2048ecdf10b7cb6c78cbea3fe91
parent1461eb7e4d6f63590374608c1388d8fbd7872be3 (diff)
parentdcbdcf353432294e9fb4da25c2ad7abf8b08f3dc (diff)
Merge "Implement export location metadata feature"2.0.0.0b2
-rw-r--r--etc/manila/policy.json4
-rw-r--r--manila/api/openstack/api_version_request.py3
-rw-r--r--manila/api/openstack/rest_api_version_history.rst5
-rw-r--r--manila/api/v2/router.py33
-rw-r--r--manila/api/v2/share_export_locations.py78
-rw-r--r--manila/api/v2/share_instance_export_locations.py67
-rw-r--r--manila/api/views/export_locations.py71
-rw-r--r--manila/api/views/share_instance.py12
-rw-r--r--manila/api/views/shares.py6
-rw-r--r--manila/db/api.py49
-rw-r--r--manila/db/migrations/alembic/versions/dda6de06349_add_export_locations_metadata.py120
-rw-r--r--manila/db/sqlalchemy/api.py323
-rw-r--r--manila/db/sqlalchemy/models.py41
-rw-r--r--manila/exception.py4
-rw-r--r--manila/share/api.py58
-rw-r--r--manila/share/drivers/generic.py10
-rw-r--r--manila/share/manager.py14
-rw-r--r--manila/tests/api/v2/test_share_export_locations.py152
-rw-r--r--manila/tests/api/v2/test_share_instance_export_locations.py121
-rw-r--r--manila/tests/api/v2/test_share_instances.py45
-rw-r--r--manila/tests/api/v2/test_shares.py13
-rw-r--r--manila/tests/db/migrations/alembic/migrations_data_checks.py81
-rw-r--r--manila/tests/db/sqlalchemy/test_api.py184
-rw-r--r--manila/tests/policy.json4
-rw-r--r--manila/tests/share/drivers/test_generic.py7
-rw-r--r--manila/tests/share/test_api.py30
-rw-r--r--manila/tests/share/test_manager.py48
-rw-r--r--manila/tests/test_exception.py7
-rw-r--r--manila_tempest_tests/config.py2
-rw-r--r--manila_tempest_tests/services/share/v2/json/shares_client.py35
-rw-r--r--manila_tempest_tests/tests/api/admin/test_export_locations.py143
-rw-r--r--manila_tempest_tests/tests/api/admin/test_export_locations_negative.py94
-rw-r--r--manila_tempest_tests/tests/api/admin/test_share_instances.py39
-rw-r--r--manila_tempest_tests/tests/api/base.py3
-rw-r--r--manila_tempest_tests/tests/api/test_shares.py12
-rw-r--r--manila_tempest_tests/tests/api/test_shares_actions.py22
-rw-r--r--manila_tempest_tests/tests/scenario/manager_share.py1
-rw-r--r--manila_tempest_tests/tests/scenario/test_share_basic_ops.py20
-rw-r--r--releasenotes/notes/add-export-locations-api-6fc6086c6a081faa.yaml6
39 files changed, 1839 insertions, 128 deletions
diff --git a/etc/manila/policy.json b/etc/manila/policy.json
index 6558dae..80eb354 100644
--- a/etc/manila/policy.json
+++ b/etc/manila/policy.json
@@ -37,11 +37,15 @@
37 "share:unmanage": "rule:admin_api", 37 "share:unmanage": "rule:admin_api",
38 "share:force_delete": "rule:admin_api", 38 "share:force_delete": "rule:admin_api",
39 "share:reset_status": "rule:admin_api", 39 "share:reset_status": "rule:admin_api",
40 "share_export_location:index": "rule:default",
41 "share_export_location:show": "rule:default",
40 42
41 "share_instance:index": "rule:admin_api", 43 "share_instance:index": "rule:admin_api",
42 "share_instance:show": "rule:admin_api", 44 "share_instance:show": "rule:admin_api",
43 "share_instance:force_delete": "rule:admin_api", 45 "share_instance:force_delete": "rule:admin_api",
44 "share_instance:reset_status": "rule:admin_api", 46 "share_instance:reset_status": "rule:admin_api",
47 "share_instance_export_location:index": "rule:admin_api",
48 "share_instance_export_location:show": "rule:admin_api",
45 49
46 "share_snapshot:create_snapshot": "rule:default", 50 "share_snapshot:create_snapshot": "rule:default",
47 "share_snapshot:delete_snapshot": "rule:default", 51 "share_snapshot:delete_snapshot": "rule:default",
diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py
index 476f75f..8b2f556 100644
--- a/manila/api/openstack/api_version_request.py
+++ b/manila/api/openstack/api_version_request.py
@@ -54,13 +54,14 @@ REST_API_VERSION_HISTORY = """
54 * 2.6 - Return share_type UUID instead of name in Share API 54 * 2.6 - Return share_type UUID instead of name in Share API
55 * 2.7 - Rename old extension-like API URLs to core-API-like 55 * 2.7 - Rename old extension-like API URLs to core-API-like
56 * 2.8 - Attr "is_public" can be set for share using API "manage" 56 * 2.8 - Attr "is_public" can be set for share using API "manage"
57 * 2.9 - Add export locations API
57""" 58"""
58 59
59# The minimum and maximum versions of the API supported 60# The minimum and maximum versions of the API supported
60# The default api version request is defined to be the 61# The default api version request is defined to be the
61# the minimum version of the API supported. 62# the minimum version of the API supported.
62_MIN_API_VERSION = "2.0" 63_MIN_API_VERSION = "2.0"
63_MAX_API_VERSION = "2.8" 64_MAX_API_VERSION = "2.9"
64DEFAULT_API_VERSION = _MIN_API_VERSION 65DEFAULT_API_VERSION = _MIN_API_VERSION
65 66
66 67
diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst
index 1fa58f7..a2257c1 100644
--- a/manila/api/openstack/rest_api_version_history.rst
+++ b/manila/api/openstack/rest_api_version_history.rst
@@ -69,3 +69,8 @@ user documentation.
692.8 692.8
70--- 70---
71 Allow to set share visibility explicitly using "manage" API. 71 Allow to set share visibility explicitly using "manage" API.
72
732.9
74---
75 Add export locations API. Remove export locations from "shares" and
76 "share instances" APIs.
diff --git a/manila/api/v2/router.py b/manila/api/v2/router.py
index 7ad60f7..b8ac74f 100644
--- a/manila/api/v2/router.py
+++ b/manila/api/v2/router.py
@@ -38,6 +38,8 @@ from manila.api.v2 import consistency_groups
38from manila.api.v2 import quota_class_sets 38from manila.api.v2 import quota_class_sets
39from manila.api.v2 import quota_sets 39from manila.api.v2 import quota_sets
40from manila.api.v2 import services 40from manila.api.v2 import services
41from manila.api.v2 import share_export_locations
42from manila.api.v2 import share_instance_export_locations
41from manila.api.v2 import share_instances 43from manila.api.v2 import share_instances
42from manila.api.v2 import share_types 44from manila.api.v2 import share_types
43from manila.api.v2 import shares 45from manila.api.v2 import shares
@@ -153,12 +155,43 @@ class APIRouter(manila.api.openstack.APIRouter):
153 collection={"detail": "GET"}, 155 collection={"detail": "GET"},
154 member={"action": "POST"}) 156 member={"action": "POST"})
155 157
158 self.resources["share_instance_export_locations"] = (
159 share_instance_export_locations.create_resource())
160 mapper.connect("share_instances",
161 ("/{project_id}/share_instances/{share_instance_id}/"
162 "export_locations"),
163 controller=self.resources[
164 "share_instance_export_locations"],
165 action="index",
166 conditions={"method": ["GET"]})
167 mapper.connect("share_instances",
168 ("/{project_id}/share_instances/{share_instance_id}/"
169 "export_locations/{export_location_uuid}"),
170 controller=self.resources[
171 "share_instance_export_locations"],
172 action="show",
173 conditions={"method": ["GET"]})
174
156 mapper.connect("share_instance", 175 mapper.connect("share_instance",
157 "/{project_id}/shares/{share_id}/instances", 176 "/{project_id}/shares/{share_id}/instances",
158 controller=self.resources["share_instances"], 177 controller=self.resources["share_instances"],
159 action="get_share_instances", 178 action="get_share_instances",
160 conditions={"method": ["GET"]}) 179 conditions={"method": ["GET"]})
161 180
181 self.resources["share_export_locations"] = (
182 share_export_locations.create_resource())
183 mapper.connect("shares",
184 "/{project_id}/shares/{share_id}/export_locations",
185 controller=self.resources["share_export_locations"],
186 action="index",
187 conditions={"method": ["GET"]})
188 mapper.connect("shares",
189 ("/{project_id}/shares/{share_id}/"
190 "export_locations/{export_location_uuid}"),
191 controller=self.resources["share_export_locations"],
192 action="show",
193 conditions={"method": ["GET"]})
194
162 self.resources["snapshots"] = share_snapshots.create_resource() 195 self.resources["snapshots"] = share_snapshots.create_resource()
163 mapper.resource("snapshot", "snapshots", 196 mapper.resource("snapshot", "snapshots",
164 controller=self.resources["snapshots"], 197 controller=self.resources["snapshots"],
diff --git a/manila/api/v2/share_export_locations.py b/manila/api/v2/share_export_locations.py
new file mode 100644
index 0000000..4975c4e
--- /dev/null
+++ b/manila/api/v2/share_export_locations.py
@@ -0,0 +1,78 @@
1# Copyright 2015 Mirantis inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16from webob import exc
17
18from manila.api.openstack import wsgi
19from manila.api.views import export_locations as export_locations_views
20from manila.db import api as db_api
21from manila import exception
22from manila.i18n import _
23
24
25class ShareExportLocationController(wsgi.Controller):
26 """The Share Export Locations API controller."""
27
28 def __init__(self):
29 self._view_builder_class = export_locations_views.ViewBuilder
30 self.resource_name = 'share_export_location'
31 super(self.__class__, self).__init__()
32
33 def _verify_share(self, context, share_id):
34 try:
35 db_api.share_get(context, share_id)
36 except exception.NotFound:
37 msg = _("Share '%s' not found.") % share_id
38 raise exc.HTTPNotFound(explanation=msg)
39
40 @wsgi.Controller.api_version('2.9')
41 @wsgi.Controller.authorize
42 def index(self, req, share_id):
43 """Return a list of export locations for share."""
44
45 context = req.environ['manila.context']
46 self._verify_share(context, share_id)
47 if context.is_admin:
48 export_locations = db_api.share_export_locations_get_by_share_id(
49 context, share_id, include_admin_only=True)
50 return self._view_builder.detail_list(export_locations)
51 else:
52 export_locations = db_api.share_export_locations_get_by_share_id(
53 context, share_id, include_admin_only=False)
54 return self._view_builder.summary_list(export_locations)
55
56 @wsgi.Controller.api_version('2.9')
57 @wsgi.Controller.authorize
58 def show(self, req, share_id, export_location_uuid):
59 """Return data about the requested export location."""
60 context = req.environ['manila.context']
61 self._verify_share(context, share_id)
62 try:
63 el = db_api.share_export_location_get_by_uuid(
64 context, export_location_uuid)
65 except exception.ExportLocationNotFound:
66 msg = _("Export location '%s' not found.") % export_location_uuid
67 raise exc.HTTPNotFound(explanation=msg)
68
69 if context.is_admin:
70 return self._view_builder.detail(el)
71 else:
72 if not el.is_admin_only:
73 return self._view_builder.summary(el)
74 raise exc.HTTPForbidden()
75
76
77def create_resource():
78 return wsgi.Resource(ShareExportLocationController())
diff --git a/manila/api/v2/share_instance_export_locations.py b/manila/api/v2/share_instance_export_locations.py
new file mode 100644
index 0000000..3779b9c
--- /dev/null
+++ b/manila/api/v2/share_instance_export_locations.py
@@ -0,0 +1,67 @@
1# Copyright 2015 Mirantis inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import six
17from webob import exc
18
19from manila.api.openstack import wsgi
20from manila.api.views import export_locations as export_locations_views
21from manila.db import api as db_api
22from manila import exception
23from manila.i18n import _
24
25
26class ShareInstanceExportLocationController(wsgi.Controller):
27 """The Share Instance Export Locations API controller."""
28
29 def __init__(self):
30 self._view_builder_class = export_locations_views.ViewBuilder
31 self.resource_name = 'share_instance_export_location'
32 super(self.__class__, self).__init__()
33
34 def _verify_share_instance(self, context, share_instance_id):
35 try:
36 db_api.share_instance_get(context, share_instance_id)
37 except exception.NotFound:
38 msg = _("Share instance '%s' not found.") % share_instance_id
39 raise exc.HTTPNotFound(explanation=msg)
40
41 @wsgi.Controller.api_version('2.9')
42 @wsgi.Controller.authorize
43 def index(self, req, share_instance_id):
44 """Return a list of export locations for the share instance."""
45 context = req.environ['manila.context']
46 self._verify_share_instance(context, share_instance_id)
47 export_locations = (
48 db_api.share_export_locations_get_by_share_instance_id(
49 context, share_instance_id))
50 return self._view_builder.detail_list(export_locations)
51
52 @wsgi.Controller.api_version('2.9')
53 @wsgi.Controller.authorize
54 def show(self, req, share_instance_id, export_location_uuid):
55 """Return data about the requested export location."""
56 context = req.environ['manila.context']
57 self._verify_share_instance(context, share_instance_id)
58 try:
59 el = db_api.share_export_location_get_by_uuid(
60 context, export_location_uuid)
61 return self._view_builder.detail(el)
62 except exception.ExportLocationNotFound as e:
63 raise exc.HTTPNotFound(explanation=six.text_type(e))
64
65
66def create_resource():
67 return wsgi.Resource(ShareInstanceExportLocationController())
diff --git a/manila/api/views/export_locations.py b/manila/api/views/export_locations.py
new file mode 100644
index 0000000..2a0b003
--- /dev/null
+++ b/manila/api/views/export_locations.py
@@ -0,0 +1,71 @@
1# Copyright (c) 2015 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16from manila.api import common
17
18
19class ViewBuilder(common.ViewBuilder):
20 """Model export-locations API responses as a python dictionary."""
21
22 _collection_name = "export_locations"
23
24 def _get_export_location_view(self, export_location, detail=False):
25 view = {
26 'uuid': export_location['uuid'],
27 'path': export_location['path'],
28 'created_at': export_location['created_at'],
29 'updated_at': export_location['updated_at'],
30 }
31 # TODO(vponomaryov): include metadata keys here as export location
32 # attributes when such appear.
33 #
34 # Example having export_location['el_metadata'] as following:
35 #
36 # {'speed': '1Gbps', 'access': 'rw'}
37 #
38 # or
39 #
40 # {'speed': '100Mbps', 'access': 'ro'}
41 #
42 # view['speed'] = export_location['el_metadata'].get('speed')
43 # view['access'] = export_location['el_metadata'].get('access')
44 if detail:
45 view['share_instance_id'] = export_location['share_instance_id']
46 view['is_admin_only'] = export_location['is_admin_only']
47 return {'export_location': view}
48
49 def summary(self, export_location):
50 """Summary view of a single export location."""
51 return self._get_export_location_view(export_location, detail=False)
52
53 def detail(self, export_location):
54 """Detailed view of a single export location."""
55 return self._get_export_location_view(export_location, detail=True)
56
57 def _list_export_locations(self, export_locations, detail=False):
58 """View of export locations list."""
59 view_method = self.detail if detail else self.summary
60 return {self._collection_name: [
61 view_method(export_location)['export_location']
62 for export_location in export_locations
63 ]}
64
65 def detail_list(self, export_locations):
66 """Detailed View of export locations list."""
67 return self._list_export_locations(export_locations, detail=True)
68
69 def summary_list(self, export_locations):
70 """Summary View of export locations list."""
71 return self._list_export_locations(export_locations, detail=False)
diff --git a/manila/api/views/share_instance.py b/manila/api/views/share_instance.py
index f23bdd8..00bebd3 100644
--- a/manila/api/views/share_instance.py
+++ b/manila/api/views/share_instance.py
@@ -18,6 +18,10 @@ class ViewBuilder(common.ViewBuilder):
18 18
19 _collection_name = 'share_instances' 19 _collection_name = 'share_instances'
20 20
21 _detail_version_modifiers = [
22 "remove_export_locations",
23 ]
24
21 def detail_list(self, request, instances): 25 def detail_list(self, request, instances):
22 """Detailed view of a list of share instances.""" 26 """Detailed view of a list of share instances."""
23 return self._list_view(self.detail, request, instances) 27 return self._list_view(self.detail, request, instances)
@@ -38,7 +42,8 @@ class ViewBuilder(common.ViewBuilder):
38 'export_location': share_instance.get('export_location'), 42 'export_location': share_instance.get('export_location'),
39 'export_locations': export_locations, 43 'export_locations': export_locations,
40 } 44 }
41 45 self.update_versioned_resource_dict(
46 request, instance_dict, share_instance)
42 return {'share_instance': instance_dict} 47 return {'share_instance': instance_dict}
43 48
44 def _list_view(self, func, request, instances): 49 def _list_view(self, func, request, instances):
@@ -54,3 +59,8 @@ class ViewBuilder(common.ViewBuilder):
54 instances_dict[self._collection_name] = instances_links 59 instances_dict[self._collection_name] = instances_links
55 60
56 return instances_dict 61 return instances_dict
62
63 @common.ViewBuilder.versioned_method("2.9")
64 def remove_export_locations(self, share_instance_dict, share_instance):
65 share_instance_dict.pop('export_location')
66 share_instance_dict.pop('export_locations')
diff --git a/manila/api/views/shares.py b/manila/api/views/shares.py
index c954854..0340d2d 100644
--- a/manila/api/views/shares.py
+++ b/manila/api/views/shares.py
@@ -25,6 +25,7 @@ class ViewBuilder(common.ViewBuilder):
25 "add_consistency_group_fields", 25 "add_consistency_group_fields",
26 "add_task_state_field", 26 "add_task_state_field",
27 "modify_share_type_field", 27 "modify_share_type_field",
28 "remove_export_locations",
28 ] 29 ]
29 30
30 def summary_list(self, request, shares): 31 def summary_list(self, request, shares):
@@ -117,6 +118,11 @@ class ViewBuilder(common.ViewBuilder):
117 'share_type': share_type, 118 'share_type': share_type,
118 }) 119 })
119 120
121 @common.ViewBuilder.versioned_method("2.9")
122 def remove_export_locations(self, share_dict, share):
123 share_dict.pop('export_location')
124 share_dict.pop('export_locations')
125
120 def _list_view(self, func, request, shares): 126 def _list_view(self, func, request, shares):
121 """Provide a view for a list of shares.""" 127 """Provide a view for a list of shares."""
122 shares_list = [func(request, share)['share'] for share in shares] 128 shares_list = [func(request, share)['share'] for share in shares]
diff --git a/manila/db/api.py b/manila/db/api.py
index e58fce6..6c0a483 100644
--- a/manila/db/api.py
+++ b/manila/db/api.py
@@ -597,17 +597,58 @@ def share_metadata_update(context, share, metadata, delete):
597 597
598################### 598###################
599 599
600def share_export_location_get_by_uuid(context, export_location_uuid):
601 """Get specific export location of a share."""
602 return IMPL.share_export_location_get_by_uuid(
603 context, export_location_uuid)
604
605
600def share_export_locations_get(context, share_id): 606def share_export_locations_get(context, share_id):
601 """Get all exports_locations of share.""" 607 """Get all export locations of a share."""
602 return IMPL.share_export_locations_get(context, share_id) 608 return IMPL.share_export_locations_get(context, share_id)
603 609
604 610
611def share_export_locations_get_by_share_id(context, share_id,
612 include_admin_only=True):
613 """Get all export locations of a share by its ID."""
614 return IMPL.share_export_locations_get_by_share_id(
615 context, share_id, include_admin_only=include_admin_only)
616
617
618def share_export_locations_get_by_share_instance_id(context,
619 share_instance_id):
620 """Get all export locations of a share instance by its ID."""
621 return IMPL.share_export_locations_get_by_share_instance_id(
622 context, share_instance_id)
623
624
605def share_export_locations_update(context, share_instance_id, export_locations, 625def share_export_locations_update(context, share_instance_id, export_locations,
606 delete=True): 626 delete=True):
607 """Update export locations of share.""" 627 """Update export locations of a share instance."""
608 return IMPL.share_export_locations_update(context, share_instance_id, 628 return IMPL.share_export_locations_update(
609 export_locations, delete) 629 context, share_instance_id, export_locations, delete)
630
631
632####################
633
634def export_location_metadata_get(context, export_location_uuid, session=None):
635 """Get all metadata of an export location."""
636 return IMPL.export_location_metadata_get(
637 context, export_location_uuid, session=session)
638
639
640def export_location_metadata_delete(context, export_location_uuid, keys,
641 session=None):
642 """Delete metadata of an export location."""
643 return IMPL.export_location_metadata_delete(
644 context, export_location_uuid, keys, session=session)
645
610 646
647def export_location_metadata_update(context, export_location_uuid, metadata,
648 delete, session=None):
649 """Update metadata of an export location."""
650 return IMPL.export_location_metadata_update(
651 context, export_location_uuid, metadata, delete, session=session)
611 652
612#################### 653####################
613 654
diff --git a/manila/db/migrations/alembic/versions/dda6de06349_add_export_locations_metadata.py b/manila/db/migrations/alembic/versions/dda6de06349_add_export_locations_metadata.py
new file mode 100644
index 0000000..75abfaa
--- /dev/null
+++ b/manila/db/migrations/alembic/versions/dda6de06349_add_export_locations_metadata.py
@@ -0,0 +1,120 @@
1# Copyright 2015 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""Add DB support for share instance export locations metadata.
17
18Revision ID: dda6de06349
19Revises: 323840a08dc4
20Create Date: 2015-11-30 13:50:15.914232
21
22"""
23
24# revision identifiers, used by Alembic.
25revision = 'dda6de06349'
26down_revision = '323840a08dc4'
27
28from alembic import op
29from oslo_log import log
30from oslo_utils import uuidutils
31import sqlalchemy as sa
32
33from manila.i18n import _LE
34
35SI_TABLE_NAME = 'share_instances'
36EL_TABLE_NAME = 'share_instance_export_locations'
37ELM_TABLE_NAME = 'share_instance_export_locations_metadata'
38LOG = log.getLogger(__name__)
39
40
41def upgrade():
42 try:
43 meta = sa.MetaData()
44 meta.bind = op.get_bind()
45
46 # Add new 'is_admin_only' column in export locations table that will be
47 # used for hiding admin export locations from common users in API.
48 op.add_column(
49 EL_TABLE_NAME,
50 sa.Column('is_admin_only', sa.Boolean, default=False))
51
52 # Create new 'uuid' column as String(36) in export locations table
53 # that will be used for API.
54 op.add_column(
55 EL_TABLE_NAME,
56 sa.Column('uuid', sa.String(36), unique=True),
57 )
58
59 # Generate UUID for each existing export location.
60 el_table = sa.Table(
61 EL_TABLE_NAME, meta,
62 sa.Column('id', sa.Integer),
63 sa.Column('uuid', sa.String(36)),
64 sa.Column('is_admin_only', sa.Boolean),
65 )
66 for record in el_table.select().execute():
67 el_table.update().values(
68 is_admin_only=False,
69 uuid=uuidutils.generate_uuid(),
70 ).where(
71 el_table.c.id == record.id,
72 ).execute()
73
74 # Make new 'uuid' column in export locations table not nullable.
75 op.alter_column(
76 EL_TABLE_NAME,
77 'uuid',
78 existing_type=sa.String(length=36),
79 nullable=False,
80 )
81 except Exception:
82 LOG.error(_LE("Failed to update '%s' table!"),
83 EL_TABLE_NAME)
84 raise
85
86 try:
87 op.create_table(
88 ELM_TABLE_NAME,
89 sa.Column('id', sa.Integer, primary_key=True),
90 sa.Column('created_at', sa.DateTime),
91 sa.Column('updated_at', sa.DateTime),
92 sa.Column('deleted_at', sa.DateTime),
93 sa.Column('deleted', sa.Integer),
94 sa.Column('export_location_id', sa.Integer,
95 sa.ForeignKey('%s.id' % EL_TABLE_NAME,
96 name="elm_id_fk"), nullable=False),
97 sa.Column('key', sa.String(length=255), nullable=False),
98 sa.Column('value', sa.String(length=1023), nullable=False),
99 sa.UniqueConstraint('export_location_id', 'key', 'deleted',
100 name="elm_el_id_uc"),
101 mysql_engine='InnoDB',
102 )
103 except Exception:
104 LOG.error(_LE("Failed to create '%s' table!"), ELM_TABLE_NAME)
105 raise
106
107
108def downgrade():
109 try:
110 op.drop_table(ELM_TABLE_NAME)
111 except Exception:
112 LOG.error(_LE("Failed to drop '%s' table!"), ELM_TABLE_NAME)
113 raise
114
115 try:
116 op.drop_column(EL_TABLE_NAME, 'is_admin_only')
117 op.drop_column(EL_TABLE_NAME, 'uuid')
118 except Exception:
119 LOG.error(_LE("Failed to update '%s' table!"), EL_TABLE_NAME)
120 raise
diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py
index 81ddce4..e488a1f 100644
--- a/manila/db/sqlalchemy/api.py
+++ b/manila/db/sqlalchemy/api.py
@@ -179,6 +179,20 @@ def require_share_exists(f):
179 return wrapper 179 return wrapper
180 180
181 181
182def require_share_instance_exists(f):
183 """Decorator to require the specified share instance to exist.
184
185 Requires the wrapped function to use context and share_instance_id as
186 their first two arguments.
187 """
188
189 def wrapper(context, share_instance_id, *args, **kwargs):
190 share_instance_get(context, share_instance_id)
191 return f(context, share_instance_id, *args, **kwargs)
192 wrapper.__name__ = f.__name__
193 return wrapper
194
195
182def model_query(context, model, *args, **kwargs): 196def model_query(context, model, *args, **kwargs):
183 """Query helper that accounts for context's `read_deleted` field. 197 """Query helper that accounts for context's `read_deleted` field.
184 198
@@ -1171,10 +1185,13 @@ def share_instance_get(context, share_instance_id, session=None,
1171 with_share_data=False): 1185 with_share_data=False):
1172 if session is None: 1186 if session is None:
1173 session = get_session() 1187 session = get_session()
1174 result = ( 1188 result = model_query(
1175 model_query(context, models.ShareInstance, session=session).filter_by( 1189 context, models.ShareInstance, session=session,
1176 id=share_instance_id).first() 1190 ).filter_by(
1177 ) 1191 id=share_instance_id,
1192 ).options(
1193 joinedload('export_locations'),
1194 ).first()
1178 if result is None: 1195 if result is None:
1179 raise exception.NotFound() 1196 raise exception.NotFound()
1180 1197
@@ -1188,10 +1205,11 @@ def share_instance_get(context, share_instance_id, session=None,
1188@require_admin_context 1205@require_admin_context
1189def share_instances_get_all(context): 1206def share_instances_get_all(context):
1190 session = get_session() 1207 session = get_session()
1191 return ( 1208 return model_query(
1192 model_query(context, models.ShareInstance, session=session, 1209 context, models.ShareInstance, session=session, read_deleted="no",
1193 read_deleted="no").all() 1210 ).options(
1194 ) 1211 joinedload('export_locations'),
1212 ).all()
1195 1213
1196 1214
1197@require_context 1215@require_context
@@ -1200,15 +1218,11 @@ def share_instance_delete(context, instance_id, session=None):
1200 session = get_session() 1218 session = get_session()
1201 1219
1202 with session.begin(): 1220 with session.begin():
1221 share_export_locations_update(context, instance_id, [], delete=True)
1203 instance_ref = share_instance_get(context, instance_id, 1222 instance_ref = share_instance_get(context, instance_id,
1204 session=session) 1223 session=session)
1205 instance_ref.soft_delete(session=session, update_status=True) 1224 instance_ref.soft_delete(session=session, update_status=True)
1206
1207 session.query(models.ShareInstanceExportLocations).filter_by(
1208 share_instance_id=instance_id).soft_delete()
1209
1210 share = share_get(context, instance_ref['share_id'], session=session) 1225 share = share_get(context, instance_ref['share_id'], session=session)
1211
1212 if len(share.instances) == 0: 1226 if len(share.instances) == 0:
1213 share.soft_delete(session=session) 1227 share.soft_delete(session=session)
1214 share_access_delete_all_by_share(context, share['id']) 1228 share_access_delete_all_by_share(context, share['id'])
@@ -2019,29 +2033,90 @@ def _share_metadata_get_item(context, share_id, key, session=None):
2019 return result 2033 return result
2020 2034
2021 2035
2022################################# 2036############################
2037# Export locations functions
2038############################
2039
2040def _share_export_locations_get(context, share_instance_ids,
2041 include_admin_only=True, session=None):
2042 session = session or get_session()
2043
2044 if not isinstance(share_instance_ids, (set, list, tuple)):
2045 share_instance_ids = (share_instance_ids, )
2046
2047 query = model_query(
2048 context,
2049 models.ShareInstanceExportLocations,
2050 session=session,
2051 read_deleted="no",
2052 ).filter(
2053 models.ShareInstanceExportLocations.share_instance_id.in_(
2054 share_instance_ids),
2055 ).order_by(
2056 "updated_at",
2057 ).options(
2058 joinedload("_el_metadata_bare"),
2059 )
2060
2061 if not include_admin_only:
2062 query = query.filter_by(is_admin_only=False)
2063 return query.all()
2064
2065
2066@require_context
2067@require_share_exists
2068def share_export_locations_get_by_share_id(context, share_id,
2069 include_admin_only=True):
2070 share = share_get(context, share_id)
2071 ids = [instance.id for instance in share.instances]
2072 rows = _share_export_locations_get(
2073 context, ids, include_admin_only=include_admin_only)
2074 return rows
2075
2076
2077@require_context
2078@require_share_instance_exists
2079def share_export_locations_get_by_share_instance_id(context,
2080 share_instance_id):
2081 rows = _share_export_locations_get(
2082 context, [share_instance_id], include_admin_only=True)
2083 return rows
2084
2023 2085
2024@require_context 2086@require_context
2025@require_share_exists 2087@require_share_exists
2026def share_export_locations_get(context, share_id): 2088def share_export_locations_get(context, share_id):
2089 # NOTE(vponomaryov): this method is kept for compatibility with
2090 # old approach. New one uses 'share_export_locations_get_by_share_id'.
2091 # Which returns list of dicts instead of list of strings, as this one does.
2027 share = share_get(context, share_id) 2092 share = share_get(context, share_id)
2028 rows = _share_export_locations_get(context, share.instance.id) 2093 rows = _share_export_locations_get(
2094 context, share.instance.id, context.is_admin)
2029 2095
2030 return [location['path'] for location in rows] 2096 return [location['path'] for location in rows]
2031 2097
2032 2098
2033def _share_export_locations_get(context, share_instance_id, session=None): 2099@require_context
2034 if not session: 2100def share_export_location_get_by_uuid(context, export_location_uuid,
2035 session = get_session() 2101 session=None):
2102 session = session or get_session()
2036 2103
2037 return ( 2104 query = model_query(
2038 model_query(context, models.ShareInstanceExportLocations, 2105 context,
2039 session=session, read_deleted="no"). 2106 models.ShareInstanceExportLocations,
2040 filter_by(share_instance_id=share_instance_id). 2107 session=session,
2041 order_by("updated_at"). 2108 read_deleted="no",
2042 all() 2109 ).filter_by(
2110 uuid=export_location_uuid,
2111 ).options(
2112 joinedload("_el_metadata_bare"),
2043 ) 2113 )
2044 2114
2115 result = query.first()
2116 if not result:
2117 raise exception.ExportLocationNotFound(uuid=export_location_uuid)
2118 return result
2119
2045 2120
2046@require_context 2121@require_context
2047@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) 2122@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@@ -2049,67 +2124,177 @@ def share_export_locations_update(context, share_instance_id, export_locations,
2049 delete): 2124 delete):
2050 # NOTE(u_glide): 2125 # NOTE(u_glide):
2051 # Backward compatibility code for drivers, 2126 # Backward compatibility code for drivers,
2052 # which returns single export_location as string 2127 # which return single export_location as string
2053 if not isinstance(export_locations, list): 2128 if not isinstance(export_locations, (list, tuple, set)):
2054 export_locations = [export_locations] 2129 export_locations = (export_locations, )
2130 export_locations_as_dicts = []
2131 for el in export_locations:
2132 # NOTE(vponomaryov): transform old export locations view to new one
2133 export_location = el
2134 if isinstance(el, six.string_types):
2135 export_location = {
2136 "path": el,
2137 "is_admin_only": False,
2138 "metadata": {},
2139 }
2140 elif isinstance(export_location, dict):
2141 if 'metadata' not in export_location:
2142 export_location['metadata'] = {}
2143 else:
2144 raise exception.ManilaException(
2145 _("Wrong export location type '%s'.") % type(export_location))
2146 export_locations_as_dicts.append(export_location)
2147 export_locations = export_locations_as_dicts
2148
2149 export_locations_paths = [el['path'] for el in export_locations]
2055 2150
2056 session = get_session() 2151 session = get_session()
2057 2152
2058 with session.begin(): 2153 current_el_rows = _share_export_locations_get(
2059 location_rows = _share_export_locations_get( 2154 context, share_instance_id, session=session)
2060 context, share_instance_id, session=session)
2061 2155
2062 def get_path_list_from_rows(rows): 2156 def get_path_list_from_rows(rows):
2063 return set([l['path'] for l in rows]) 2157 return set([l['path'] for l in rows])
2064 2158
2065 current_locations = get_path_list_from_rows(location_rows) 2159 current_el_paths = get_path_list_from_rows(current_el_rows)
2066 2160
2067 def create_indexed_time_dict(key_list): 2161 def create_indexed_time_dict(key_list):
2068 base = timeutils.utcnow() 2162 base = timeutils.utcnow()
2069 return { 2163 return {
2070 # NOTE(u_glide): Incrementing timestamp by microseconds to make 2164 # NOTE(u_glide): Incrementing timestamp by microseconds to make
2071 # timestamp order match index order. 2165 # timestamp order match index order.
2072 key: base + datetime.timedelta(microseconds=index) 2166 key: base + datetime.timedelta(microseconds=index)
2073 for index, key in enumerate(key_list) 2167 for index, key in enumerate(key_list)
2074 } 2168 }
2075 2169
2076 indexed_update_time = create_indexed_time_dict(export_locations) 2170 indexed_update_time = create_indexed_time_dict(export_locations_paths)
2077 2171
2078 for location in location_rows: 2172 for el in current_el_rows:
2079 if delete and location['path'] not in export_locations: 2173 if delete and el['path'] not in export_locations_paths:
2080 location.soft_delete(session) 2174 export_location_metadata_delete(context, el['uuid'])
2081 else: 2175 el.soft_delete(session)
2082 updated_at = indexed_update_time[location['path']] 2176 else:
2083 location.update({ 2177 updated_at = indexed_update_time[el['path']]
2084 'updated_at': updated_at, 2178 el.update({
2085 'deleted': 0, 2179 'updated_at': updated_at,
2086 }) 2180 'deleted': 0,
2181 })
2182 el.save(session=session)
2183 if el['el_metadata']:
2184 export_location_metadata_update(
2185 context, el['uuid'], el['el_metadata'], session=session)
2186
2187 # Now add new export locations
2188 for el in export_locations:
2189 if el['path'] in current_el_paths:
2190 # Already updated
2191 continue
2087 2192
2088 location.save(session=session) 2193 location_ref = models.ShareInstanceExportLocations()
2194 location_ref.update({
2195 'uuid': uuidutils.generate_uuid(),
2196 'path': el['path'],
2197 'share_instance_id': share_instance_id,
2198 'updated_at': indexed_update_time[el['path']],
2199 'deleted': 0,
2200 'is_admin_only': el.get('is_admin_only', False),
2201 })
2202 location_ref.save(session=session)
2203 if not el.get('metadata'):
2204 continue
2205 export_location_metadata_update(
2206 context, location_ref['uuid'], el.get('metadata'), session=session)
2089 2207
2090 # Now add new export locations 2208 return get_path_list_from_rows(_share_export_locations_get(
2091 for path in export_locations: 2209 context, share_instance_id, session=session))
2092 if path in current_locations:
2093 # Already updated
2094 continue
2095 2210
2096 location_ref = models.ShareInstanceExportLocations() 2211
2097 location_ref.update({ 2212#####################################
2098 'path': path, 2213# Export locations metadata functions
2099 'share_instance_id': share_instance_id, 2214#####################################
2100 'updated_at': indexed_update_time[path], 2215
2101 'deleted': 0, 2216def _export_location_metadata_get_query(context, export_location_uuid,
2217 session=None):
2218 session = session or get_session()
2219 export_location_id = share_export_location_get_by_uuid(
2220 context, export_location_uuid).id
2221
2222 return model_query(
2223 context, models.ShareInstanceExportLocationsMetadata, session=session,
2224 read_deleted="no",
2225 ).filter_by(
2226 export_location_id=export_location_id,
2227 )
2228
2229
2230@require_context
2231def export_location_metadata_get(context, export_location_uuid, session=None):
2232 rows = _export_location_metadata_get_query(
2233 context, export_location_uuid, session=session).all()
2234 result = {}
2235 for row in rows:
2236 result[row["key"]] = row["value"]
2237 return result
2238
2239
2240@require_context
2241def export_location_metadata_delete(context, export_location_uuid, keys=None):
2242 session = get_session()
2243 metadata = _export_location_metadata_get_query(
2244 context, export_location_uuid, session=session,
2245 )
2246 # NOTE(vponomaryov): if keys is None then we delete all metadata.
2247 if keys is not None:
2248 keys = keys if isinstance(keys, (list, set, tuple)) else (keys, )
2249 metadata = metadata.filter(
2250 models.ShareInstanceExportLocationsMetadata.key.in_(keys))
2251 metadata = metadata.all()
2252 for meta_ref in metadata:
2253 meta_ref.soft_delete(session=session)
2254
2255
2256@require_context
2257@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
2258def export_location_metadata_update(context, export_location_uuid, metadata,
2259 delete=False, session=None):
2260 session = session or get_session()
2261 if delete:
2262 original_metadata = export_location_metadata_get(
2263 context, export_location_uuid, session=session)
2264 keys_for_deletion = set(original_metadata).difference(metadata)
2265 if keys_for_deletion:
2266 export_location_metadata_delete(
2267 context, export_location_uuid, keys=keys_for_deletion)
2268
2269 el = share_export_location_get_by_uuid(context, export_location_uuid)
2270 for meta_key, meta_value in metadata.items():
2271 # NOTE(vponomaryov): we should use separate session
2272 # for each meta_ref because of autoincrement of integer primary key
2273 # that will not take effect using one session and we will rewrite,
2274 # in that case, single record - first one added with this call.
2275 session = get_session()
2276 item = {"value": meta_value, "updated_at": timeutils.utcnow()}
2277
2278 meta_ref = _export_location_metadata_get_query(
2279 context, export_location_uuid, session=session,
2280 ).filter_by(
2281 key=meta_key,
2282 ).first()
2283
2284 if not meta_ref:
2285 meta_ref = models.ShareInstanceExportLocationsMetadata()
2286 item.update({
2287 "key": meta_key,
2288 "export_location_id": el.id,
2102 }) 2289 })
2103 location_ref.save(session=session)
2104 2290
2105 if delete: 2291 meta_ref.update(item)
2106 return export_locations 2292 meta_ref.save(session=session)
2107 2293
2108 return get_path_list_from_rows(_share_export_locations_get( 2294 return metadata
2109 context, share_instance_id, session=session))
2110 2295
2111 2296
2112################################# 2297###################################
2113 2298
2114 2299
2115@require_context 2300@require_context
diff --git a/manila/db/sqlalchemy/models.py b/manila/db/sqlalchemy/models.py
index 4bf98e4..824d52e 100644
--- a/manila/db/sqlalchemy/models.py
+++ b/manila/db/sqlalchemy/models.py
@@ -363,13 +363,52 @@ class ShareInstance(BASE, ManilaBase):
363 363
364 364
365class ShareInstanceExportLocations(BASE, ManilaBase): 365class ShareInstanceExportLocations(BASE, ManilaBase):
366 """Represents export locations of shares.""" 366 """Represents export locations of share instances."""
367 __tablename__ = 'share_instance_export_locations' 367 __tablename__ = 'share_instance_export_locations'
368 368
369 _extra_keys = ['el_metadata', ]
370
371 @property
372 def el_metadata(self):
373 el_metadata = {}
374 for meta in self._el_metadata_bare: # pylint: disable=E1101
375 el_metadata[meta['key']] = meta['value']
376 return el_metadata
377
369 id = Column(Integer, primary_key=True) 378 id = Column(Integer, primary_key=True)
379 uuid = Column(String(36), nullable=False, unique=True)
370 share_instance_id = Column( 380 share_instance_id = Column(
371 String(36), ForeignKey('share_instances.id'), nullable=False) 381 String(36), ForeignKey('share_instances.id'), nullable=False)
372 path = Column(String(2000)) 382 path = Column(String(2000))
383 is_admin_only = Column(Boolean, default=False, nullable=False)
384
385
386class ShareInstanceExportLocationsMetadata(BASE, ManilaBase):
387 """Represents export location metadata of share instances."""
388 __tablename__ = "share_instance_export_locations_metadata"
389
390 _extra_keys = ['export_location_uuid', ]
391
392 id = Column(Integer, primary_key=True)
393 export_location_id = Column(
394 Integer,
395 ForeignKey("share_instance_export_locations.id"), nullable=False)
396 key = Column(String(255), nullable=False)
397 value = Column(String(1023), nullable=False)
398 export_location = orm.relationship(
399 ShareInstanceExportLocations,
400 backref="_el_metadata_bare",
401 foreign_keys=export_location_id,
402 lazy='immediate',
403 primaryjoin="and_("
404 "%(cls_name)s.export_location_id == "
405 "ShareInstanceExportLocations.id,"
406 "%(cls_name)s.deleted == 0)" % {
407 "cls_name": "ShareInstanceExportLocationsMetadata"})
408
409 @property
410 def export_location_uuid(self):
411 return self.export_location.uuid # pylint: disable=E1101
373 412
374 413
375class ShareTypes(BASE, ManilaBase): 414class ShareTypes(BASE, ManilaBase):
diff --git a/manila/exception.py b/manila/exception.py
index 9ee4454..c56dfe9 100644
--- a/manila/exception.py
+++ b/manila/exception.py
@@ -422,6 +422,10 @@ class ShareBackendException(ManilaException):
422 message = _("Share backend error: %(msg)s.") 422 message = _("Share backend error: %(msg)s.")
423 423
424 424
425class ExportLocationNotFound(NotFound):
426 message = _("Export location %(uuid)s could not be found.")
427
428
425class ShareSnapshotNotFound(NotFound): 429class ShareSnapshotNotFound(NotFound):
426 message = _("Snapshot %(snapshot_id)s could not be found.") 430 message = _("Snapshot %(snapshot_id)s could not be found.")
427 431
diff --git a/manila/share/api.py b/manila/share/api.py
index 70f5cab..3810f15 100644
--- a/manila/share/api.py
+++ b/manila/share/api.py
@@ -289,10 +289,29 @@ class API(base.Base):
289 # NOTE(ameade): Do not cast to driver if creating from cgsnapshot 289 # NOTE(ameade): Do not cast to driver if creating from cgsnapshot
290 return 290 return
291 291
292 share_dict = share.to_dict() 292 share_properties = {
293 share_dict.update( 293 'size': share['size'],
294 {'metadata': self.db.share_metadata_get(context, share['id'])} 294 'user_id': share['user_id'],
295 ) 295 'project_id': share['project_id'],
296 'metadata': self.db.share_metadata_get(context, share['id']),
297 'share_server_id': share['share_server_id'],
298 'snapshot_support': share['snapshot_support'],
299 'share_proto': share['share_proto'],
300 'share_type_id': share['share_type_id'],
301 'is_public': share['is_public'],
302 'consistency_group_id': share['consistency_group_id'],
303 'source_cgsnapshot_member_id': share[
304 'source_cgsnapshot_member_id'],
305 'snapshot_id': share['snapshot_id'],
306 }
307 share_instance_properties = {
308 'availability_zone_id': share_instance['availability_zone_id'],
309 'share_network_id': share_instance['share_network_id'],
310 'share_server_id': share_instance['share_server_id'],
311 'share_id': share_instance['share_id'],
312 'host': share_instance['host'],
313 'status': share_instance['status'],
314 }
296 315
297 share_type = None 316 share_type = None
298 if share['share_type_id']: 317 if share['share_type_id']:
@@ -300,8 +319,8 @@ class API(base.Base):
300 context, share['share_type_id']) 319 context, share['share_type_id'])
301 320
302 request_spec = { 321 request_spec = {
303 'share_properties': share_dict, 322 'share_properties': share_properties,
304 'share_instance_properties': share_instance.to_dict(), 323 'share_instance_properties': share_instance_properties,
305 'share_proto': share['share_proto'], 324 'share_proto': share['share_proto'],
306 'share_id': share['id'], 325 'share_id': share['id'],
307 'snapshot_id': share['snapshot_id'], 326 'snapshot_id': share['snapshot_id'],
@@ -599,8 +618,31 @@ class API(base.Base):
599 share_type_id = share['share_type_id'] 618 share_type_id = share['share_type_id']
600 if share_type_id: 619 if share_type_id:
601 share_type = share_types.get_share_type(context, share_type_id) 620 share_type = share_types.get_share_type(context, share_type_id)
602 request_spec = {'share_properties': share, 621
603 'share_instance_properties': share_instance.to_dict(), 622 share_properties = {
623 'size': share['size'],
624 'user_id': share['user_id'],
625 'project_id': share['project_id'],
626 'share_server_id': share['share_server_id'],
627 'snapshot_support': share['snapshot_support'],
628 'share_proto': share['share_proto'],
629 'share_type_id': share['share_type_id'],
630 'is_public': share['is_public'],
631 'consistency_group_id': share['consistency_group_id'],
632 'source_cgsnapshot_member_id': share[
633 'source_cgsnapshot_member_id'],
634 'snapshot_id': share['snapshot_id'],
635 }
636 share_instance_properties = {
637 'availability_zone_id': share_instance['availability_zone_id'],
638 'share_network_id': share_instance['share_network_id'],
639 'share_server_id': share_instance['share_server_id'],
640 'share_id': share_instance['share_id'],
641 'host': share_instance['host'],
642 'status': share_instance['status'],
643 }
644 request_spec = {'share_properties': share_properties,
645 'share_instance_properties': share_instance_properties,
604 'share_type': share_type, 646 'share_type': share_type,
605 'share_id': share['id']} 647 'share_id': share['id']}
606 648
diff --git a/manila/share/drivers/generic.py b/manila/share/drivers/generic.py
index 99c02eb..52fb36f 100644
--- a/manila/share/drivers/generic.py
+++ b/manila/share/drivers/generic.py
@@ -242,7 +242,15 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
242 location = helper.create_export( 242 location = helper.create_export(
243 server_details, 243 server_details,
244 share['name']) 244 share['name'])
245 return location 245 return {
246 "path": location,
247 "is_admin_only": False,
248 "metadata": {
249 # TODO(vponomaryov): remove this fake metadata when proper
250 # appears.
251 "export_location_metadata_example": "example",
252 },
253 }
246 254
247 def _format_device(self, server_details, volume): 255 def _format_device(self, server_details, volume):
248 """Formats device attached to the service vm.""" 256 """Formats device attached to the service vm."""
diff --git a/manila/share/manager.py b/manila/share/manager.py
index 8d2de97..2e951b8 100644
--- a/manila/share/manager.py
+++ b/manila/share/manager.py
@@ -573,6 +573,13 @@ class ShareManager(manager.SchedulerDependentManager):
573 573
574 share_server = self._get_share_server(ctxt.elevated(), 574 share_server = self._get_share_server(ctxt.elevated(),
575 share_instance) 575 share_instance)
576 share_server = {
577 'id': share_server['id'],
578 'share_network_id': share_server['share_network_id'],
579 'host': share_server['host'],
580 'status': share_server['status'],
581 'backend_details': share_server['backend_details'],
582 } if share_server else share_server
576 583
577 dest_driver_migration_info = rpcapi.get_driver_migration_info( 584 dest_driver_migration_info = rpcapi.get_driver_migration_info(
578 ctxt, share_instance, share_server) 585 ctxt, share_instance, share_server)
@@ -663,6 +670,13 @@ class ShareManager(manager.SchedulerDependentManager):
663 share_instance) 670 share_instance)
664 new_share_server = self._get_share_server(context.elevated(), 671 new_share_server = self._get_share_server(context.elevated(),
665 new_share_instance) 672 new_share_instance)
673 new_share_server = {
674 'id': new_share_server['id'],
675 'share_network_id': new_share_server['share_network_id'],
676 'host': new_share_server['host'],
677 'status': new_share_server['status'],
678 'backend_details': new_share_server['backend_details'],
679 } if new_share_server else new_share_server
666 680
667 src_migration_info = self.driver.get_migration_info( 681 src_migration_info = self.driver.get_migration_info(
668 context, share_instance, share_server) 682 context, share_instance, share_server)
diff --git a/manila/tests/api/v2/test_share_export_locations.py b/manila/tests/api/v2/test_share_export_locations.py
new file mode 100644
index 0000000..ccafb83
--- /dev/null
+++ b/manila/tests/api/v2/test_share_export_locations.py
@@ -0,0 +1,152 @@
1# Copyright (c) 2015 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import ddt
17import mock
18from webob import exc
19
20from manila.api.v2 import share_export_locations as export_locations
21from manila import context
22from manila import db
23from manila import exception
24from manila import policy
25from manila import test
26from manila.tests.api import fakes
27from manila.tests import db_utils
28
29
30@ddt.ddt
31class ShareExportLocationsAPITest(test.TestCase):
32
33 def _get_request(self, version="2.9", use_admin_context=True):
34 req = fakes.HTTPRequest.blank(
35 '/v2/shares/%s/export_locations' % self.share_instance_id,
36 version=version, use_admin_context=use_admin_context)
37 return req
38
39 def setUp(self):
40 super(self.__class__, self).setUp()
41 self.controller = (
42 export_locations.ShareExportLocationController())
43 self.resource_name = self.controller.resource_name
44 self.ctxt = {
45 'admin': context.RequestContext('admin', 'fake', True),
46 'user': context.RequestContext('fake', 'fake'),
47 }
48 self.mock_policy_check = self.mock_object(
49 policy, 'check_policy', mock.Mock(return_value=True))
50 self.share = db_utils.create_share()
51 self.share_instance_id = self.share.instance.id
52 self.req = self._get_request()
53 paths = ['fake1/1/', 'fake2/2', 'fake3/3']
54 db.share_export_locations_update(
55 self.ctxt['admin'], self.share_instance_id, paths, False)
56
57 @ddt.data('admin', 'user')
58 def test_list_and_show(self, role):
59 req = self._get_request(use_admin_context=(role == 'admin'))
60 index_result = self.controller.index(req, self.share['id'])
61
62 self.assertIn('export_locations', index_result)
63 self.assertEqual(1, len(index_result))
64 self.assertEqual(3, len(index_result['export_locations']))
65
66 for index_el in index_result['export_locations']:
67 self.assertIn('uuid', index_el)
68 show_result = self.controller.show(
69 req, self.share['id'], index_el['uuid'])
70 self.assertIn('export_location', show_result)
71 self.assertEqual(1, len(show_result))
72 expected_keys = [
73 'created_at', 'updated_at', 'uuid', 'path',
74 ]
75 if role == 'admin':
76 expected_keys.extend(['share_instance_id', 'is_admin_only'])
77 for el in (index_el, show_result['export_location']):
78 self.assertEqual(len(expected_keys), len(el))
79 for key in expected_keys:
80 self.assertIn(key, el)
81
82 for key in expected_keys:
83 self.assertEqual(
84 index_el[key], show_result['export_location'][key])
85
86 def test_list_export_locations_share_not_found(self):
87 self.assertRaises(
88 exc.HTTPNotFound,
89 self.controller.index,
90 self.req, 'inexistent_share_id',
91 )
92
93 def test_show_export_location_share_not_found(self):
94 index_result = self.controller.index(self.req, self.share['id'])
95 el_uuid = index_result['export_locations'][0]['uuid']
96 self.assertRaises(
97 exc.HTTPNotFound,
98 self.controller.show,
99 self.req, 'inexistent_share_id', el_uuid,
100 )
101
102 def test_show_export_location_not_found(self):
103 self.assertRaises(
104 exc.HTTPNotFound,
105 self.controller.show,
106 self.req, self.share['id'], 'inexistent_export_location',
107 )
108
109 def test_get_admin_export_location(self):
110 el_data = {
111 'path': '/admin/export/location',
112 'is_admin_only': True,
113 'metadata': {'foo': 'bar'},
114 }
115 db.share_export_locations_update(
116 self.ctxt['admin'], self.share_instance_id, el_data, True)
117 index_result = self.controller.index(self.req, self.share['id'])
118 el_uuid = index_result['export_locations'][0]['uuid']
119
120 # Not found for member
121 member_req = self._get_request(use_admin_context=False)
122 self.assertRaises(
123 exc.HTTPForbidden,
124 self.controller.show,
125 member_req, self.share['id'], el_uuid,
126 )
127
128 # Ok for admin
129 el = self.controller.show(self.req, self.share['id'], el_uuid)
130 for k, v in el.items():
131 self.assertEqual(v, el[k])
132
133 @ddt.data('1.0', '2.0', '2.8')
134 def test_list_with_unsupported_version(self, version):
135 self.assertRaises(
136 exception.VersionNotFoundForAPIMethod,
137 self.controller.index,
138 self._get_request(version),
139 self.share_instance_id,
140 )
141
142 @ddt.data('1.0', '2.0', '2.8')
143 def test_show_with_unsupported_version(self, version):
144 index_result = self.controller.index(self.req, self.share['id'])
145
146 self.assertRaises(
147 exception.VersionNotFoundForAPIMethod,
148 self.controller.show,
149 self._get_request(version),
150 self.share['id'],
151 index_result['export_locations'][0]['uuid']
152 )
diff --git a/manila/tests/api/v2/test_share_instance_export_locations.py b/manila/tests/api/v2/test_share_instance_export_locations.py
new file mode 100644
index 0000000..b90f8bc
--- /dev/null
+++ b/manila/tests/api/v2/test_share_instance_export_locations.py
@@ -0,0 +1,121 @@
1# Copyright (c) 2015 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import ddt
17import mock
18from webob import exc
19
20from manila.api.v2 import share_instance_export_locations as export_locations
21from manila import context
22from manila import db
23from manila import exception
24from manila import policy
25from manila import test
26from manila.tests.api import fakes
27from manila.tests import db_utils
28
29
30@ddt.ddt
31class ShareInstanceExportLocationsAPITest(test.TestCase):
32
33 def _get_request(self, version="2.9", use_admin_context=True):
34 req = fakes.HTTPRequest.blank(
35 '/v2/share_instances/%s/export_locations' % self.share_instance_id,
36 version=version, use_admin_context=use_admin_context)
37 return req
38
39 def setUp(self):
40 super(self.__class__, self).setUp()
41 self.controller = (
42 export_locations.ShareInstanceExportLocationController())
43 self.resource_name = self.controller.resource_name
44 self.ctxt = {
45 'admin': context.RequestContext('admin', 'fake', True),
46 'user': context.RequestContext('fake', 'fake'),
47 }
48 self.mock_policy_check = self.mock_object(
49 policy, 'check_policy', mock.Mock(return_value=True))
50 self.share = db_utils.create_share()
51 self.share_instance_id = self.share.instance.id
52 self.req = self._get_request()
53 paths = ['fake1/1/', 'fake2/2', 'fake3/3']
54 db.share_export_locations_update(
55 self.ctxt['admin'], self.share_instance_id, paths, False)
56
57 @ddt.data('admin', 'user')
58 def test_list_and_show(self, role):
59 req = self._get_request(use_admin_context=(role == 'admin'))
60 index_result = self.controller.index(req, self.share_instance_id)
61
62 self.assertIn('export_locations', index_result)
63 self.assertEqual(1, len(index_result))
64 self.assertEqual(3, len(index_result['export_locations']))
65
66 for index_el in index_result['export_locations']:
67 self.assertIn('uuid', index_el)
68 show_result = self.controller.show(
69 req, self.share_instance_id, index_el['uuid'])
70 self.assertIn('export_location', show_result)
71 self.assertEqual(1, len(show_result))
72 expected_keys = (
73 'created_at', 'updated_at', 'uuid', 'path',
74 'share_instance_id', 'is_admin_only',
75 )
76 for el in (index_el, show_result['export_location']):
77 self.assertEqual(len(expected_keys), len(el))
78 for key in expected_keys:
79 self.assertIn(key, el)
80
81 for key in expected_keys:
82 self.assertEqual(
83 index_el[key], show_result['export_location'][key])
84
85 def test_list_export_locations_share_instance_not_found(self):
86 self.assertRaises(
87 exc.HTTPNotFound,
88 self.controller.index,
89 self.req, 'inexistent_share_instance_id',
90 )
91
92 def test_show_export_location_share_instance_not_found(self):
93 index_result = self.controller.index(self.req, self.share_instance_id)
94 el_uuid = index_result['export_locations'][0]['uuid']
95
96 self.assertRaises(
97 exc.HTTPNotFound,
98 self.controller.show,
99 self.req, 'inexistent_share_id', el_uuid,
100 )
101
102 @ddt.data('1.0', '2.0', '2.8')
103 def test_list_with_unsupported_version(self, version):
104 self.assertRaises(
105 exception.VersionNotFoundForAPIMethod,
106 self.controller.index,
107 self._get_request(version),
108 self.share_instance_id,
109 )
110
111 @ddt.data('1.0', '2.0', '2.8')
112 def test_show_with_unsupported_version(self, version):
113 index_result = self.controller.index(self.req, self.share_instance_id)
114
115 self.assertRaises(
116 exception.VersionNotFoundForAPIMethod,
117 self.controller.show,
118 self._get_request(version),
119 self.share_instance_id,
120 index_result['export_locations'][0]['uuid']
121 )
diff --git a/manila/tests/api/v2/test_share_instances.py b/manila/tests/api/v2/test_share_instances.py
index b6840b9..a786f18 100644
--- a/manila/tests/api/v2/test_share_instances.py
+++ b/manila/tests/api/v2/test_share_instances.py
@@ -17,6 +17,7 @@ from oslo_serialization import jsonutils
17import six 17import six
18from webob import exc as webob_exc 18from webob import exc as webob_exc
19 19
20from manila.api.openstack import api_version_request
20from manila.api.v2 import share_instances 21from manila.api.v2 import share_instances
21from manila.common import constants 22from manila.common import constants
22from manila import context 23from manila import context
@@ -56,10 +57,10 @@ class ShareInstancesAPITest(test.TestCase):
56 version=version) 57 version=version)
57 return instance, req 58 return instance, req
58 59
59 def _get_request(self, uri, context=None): 60 def _get_request(self, uri, context=None, version="2.3"):
60 if context is None: 61 if context is None:
61 context = self.admin_context 62 context = self.admin_context
62 req = fakes.HTTPRequest.blank('/shares', version="2.3") 63 req = fakes.HTTPRequest.blank('/shares', version=version)
63 req.environ['manila.context'] = context 64 req.environ['manila.context'] = context
64 return req 65 return req
65 66
@@ -94,10 +95,37 @@ class ShareInstancesAPITest(test.TestCase):
94 self.mock_policy_check.assert_called_once_with( 95 self.mock_policy_check.assert_called_once_with(
95 self.admin_context, self.resource_name, 'show') 96 self.admin_context, self.resource_name, 'show')
96 97
97 def test_get_share_instances(self): 98 def test_show_with_export_locations(self):
99 test_instance = db_utils.create_share(size=1).instance
100 req = self._get_request('fake', version="2.8")
101 id = test_instance['id']
102
103 actual_result = self.controller.show(req, id)
104
105 self.assertEqual(id, actual_result['share_instance']['id'])
106 self.assertIn("export_location", actual_result['share_instance'])
107 self.assertIn("export_locations", actual_result['share_instance'])
108 self.mock_policy_check.assert_called_once_with(
109 self.admin_context, self.resource_name, 'show')
110
111 def test_show_without_export_locations(self):
112 test_instance = db_utils.create_share(size=1).instance
113 req = self._get_request('fake', version="2.9")
114 id = test_instance['id']
115
116 actual_result = self.controller.show(req, id)
117
118 self.assertEqual(id, actual_result['share_instance']['id'])
119 self.assertNotIn("export_location", actual_result['share_instance'])
120 self.assertNotIn("export_locations", actual_result['share_instance'])
121 self.mock_policy_check.assert_called_once_with(
122 self.admin_context, self.resource_name, 'show')
123
124 @ddt.data("2.3", "2.8", "2.9")
125 def test_get_share_instances(self, version):
98 test_share = db_utils.create_share(size=1) 126 test_share = db_utils.create_share(size=1)
99 id = test_share['id'] 127 id = test_share['id']
100 req = self._get_request('fake') 128 req = self._get_request('fake', version=version)
101 req_context = req.environ['manila.context'] 129 req_context = req.environ['manila.context']
102 share_policy_check_call = mock.call( 130 share_policy_check_call = mock.call(
103 req_context, 'share', 'get', mock.ANY) 131 req_context, 'share', 'get', mock.ANY)
@@ -110,6 +138,15 @@ class ShareInstancesAPITest(test.TestCase):
110 [test_share.instance], 138 [test_share.instance],
111 actual_result['share_instances'] 139 actual_result['share_instances']
112 ) 140 )
141 self.assertEqual(1, len(actual_result.get("share_instances", 0)))
142 for instance in actual_result["share_instances"]:
143 if (api_version_request.APIVersionRequest(version) >
144 api_version_request.APIVersionRequest("2.8")):
145 assert_method = self.assertNotIn
146 else:
147 assert_method = self.assertIn
148 assert_method("export_location", instance)
149 assert_method("export_locations", instance)
113 self.mock_policy_check.assert_has_calls([ 150 self.mock_policy_check.assert_has_calls([
114 get_instances_policy_check_call, share_policy_check_call]) 151 get_instances_policy_check_call, share_policy_check_call])
115 152
diff --git a/manila/tests/api/v2/test_shares.py b/manila/tests/api/v2/test_shares.py
index d795a1d..ffabd5d 100644
--- a/manila/tests/api/v2/test_shares.py
+++ b/manila/tests/api/v2/test_shares.py
@@ -814,6 +814,19 @@ class ShareAPITest(test.TestCase):
814 expected['shares'][0]['task_state'] = None 814 expected['shares'][0]['task_state'] = None
815 self._list_detail_test_common(req, expected) 815 self._list_detail_test_common(req, expected)
816 816
817 def test_share_list_detail_without_export_locations(self):
818 env = {'QUERY_STRING': 'name=Share+Test+Name'}
819 req = fakes.HTTPRequest.blank('/shares/detail', environ=env,
820 version="2.9")
821 expected = self._list_detail_common_expected()
822 expected['shares'][0]['consistency_group_id'] = None
823 expected['shares'][0]['source_cgsnapshot_member_id'] = None
824 expected['shares'][0]['task_state'] = None
825 expected['shares'][0]['share_type_name'] = None
826 expected['shares'][0].pop('export_location')
827 expected['shares'][0].pop('export_locations')
828 self._list_detail_test_common(req, expected)
829
817 def test_remove_invalid_options(self): 830 def test_remove_invalid_options(self):
818 ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False) 831 ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False)
819 search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'} 832 search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}
diff --git a/manila/tests/db/migrations/alembic/migrations_data_checks.py b/manila/tests/db/migrations/alembic/migrations_data_checks.py
index f04aa67..c54ab0b 100644
--- a/manila/tests/db/migrations/alembic/migrations_data_checks.py
+++ b/manila/tests/db/migrations/alembic/migrations_data_checks.py
@@ -37,6 +37,7 @@ import abc
37 37
38from oslo_utils import uuidutils 38from oslo_utils import uuidutils
39import six 39import six
40from sqlalchemy import exc as sa_exc
40 41
41from manila.db.migrations import utils 42from manila.db.migrations import utils
42 43
@@ -172,3 +173,83 @@ class AvailabilityZoneMigrationChecks(BaseMigrationChecks):
172 self.test_case.assertIn( 173 self.test_case.assertIn(
173 service.availability_zone, self.valid_az_names 174 service.availability_zone, self.valid_az_names
174 ) 175 )
176
177
178@map_to_migration('dda6de06349')
179class ShareInstanceExportLocationMetadataChecks(BaseMigrationChecks):
180 el_table_name = 'share_instance_export_locations'
181 elm_table_name = 'share_instance_export_locations_metadata'
182
183 def setup_upgrade_data(self, engine):
184 # Setup shares
185 share_fixture = [{'id': 'foo_share_id'}, {'id': 'bar_share_id'}]
186 share_table = utils.load_table('shares', engine)
187 for fixture in share_fixture:
188 engine.execute(share_table.insert(fixture))
189
190 # Setup share instances
191 si_fixture = [
192 {'id': 'foo_share_instance_id_oof',
193 'share_id': share_fixture[0]['id']},
194 {'id': 'bar_share_instance_id_rab',
195 'share_id': share_fixture[1]['id']},
196 ]
197 si_table = utils.load_table('share_instances', engine)
198 for fixture in si_fixture:
199 engine.execute(si_table.insert(fixture))
200
201 # Setup export locations
202 el_fixture = [
203 {'id': 1, 'path': '/1', 'share_instance_id': si_fixture[0]['id']},
204 {'id': 2, 'path': '/2', 'share_instance_id': si_fixture[1]['id']},
205 ]
206 el_table = utils.load_table(self.el_table_name, engine)
207 for fixture in el_fixture:
208 engine.execute(el_table.insert(fixture))
209
210 def check_upgrade(self, engine, data):
211 el_table = utils.load_table(
212 'share_instance_export_locations', engine)
213 for el in engine.execute(el_table.select()):
214 self.test_case.assertTrue(hasattr(el, 'is_admin_only'))
215 self.test_case.assertTrue(hasattr(el, 'uuid'))
216 self.test_case.assertEqual(False, el.is_admin_only)
217 self.test_case.assertTrue(uuidutils.is_uuid_like(el.uuid))
218
219 # Write export location metadata
220 el_metadata = [
221 {'key': 'foo_key', 'value': 'foo_value', 'export_location_id': 1},
222 {'key': 'bar_key', 'value': 'bar_value', 'export_location_id': 2},
223 ]
224 elm_table = utils.load_table(self.elm_table_name, engine)
225 engine.execute(elm_table.insert(el_metadata))
226
227 # Verify values of written metadata
228 for el_meta_datum in el_metadata:
229 el_id = el_meta_datum['export_location_id']
230 records = engine.execute(elm_table.select().where(
231 elm_table.c.export_location_id == el_id))
232 self.test_case.assertEqual(1, records.rowcount)
233 record = records.first()
234
235 expected_keys = (
236 'id', 'created_at', 'updated_at', 'deleted_at', 'deleted',
237 'export_location_id', 'key', 'value',
238 )
239 self.test_case.assertEqual(len(expected_keys), len(record.keys()))
240 for key in expected_keys:
241 self.test_case.assertIn(key, record.keys())
242
243 for k, v in el_meta_datum.items():
244 self.test_case.assertTrue(hasattr(record, k))
245 self.test_case.assertEqual(v, getattr(record, k))
246
247 def check_downgrade(self, engine):
248 el_table = utils.load_table(
249 'share_instance_export_locations', engine)
250 for el in engine.execute(el_table.select()):
251 self.test_case.assertFalse(hasattr(el, 'is_admin_only'))
252 self.test_case.assertFalse(hasattr(el, 'uuid'))
253 self.test_case.assertRaises(
254 sa_exc.NoSuchTableError,
255 utils.load_table, self.elm_table_name, engine)
diff --git a/manila/tests/db/sqlalchemy/test_api.py b/manila/tests/db/sqlalchemy/test_api.py
index b83b882..1e45148 100644
--- a/manila/tests/db/sqlalchemy/test_api.py
+++ b/manila/tests/db/sqlalchemy/test_api.py
@@ -573,8 +573,7 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
573class ShareExportLocationsDatabaseAPITestCase(test.TestCase): 573class ShareExportLocationsDatabaseAPITestCase(test.TestCase):
574 574
575 def setUp(self): 575 def setUp(self):
576 """Run before each test.""" 576 super(self.__class__, self).setUp()
577 super(ShareExportLocationsDatabaseAPITestCase, self).setUp()
578 self.ctxt = context.get_admin_context() 577 self.ctxt = context.get_admin_context()
579 578
580 def test_update_valid_order(self): 579 def test_update_valid_order(self):
@@ -605,6 +604,187 @@ class ShareExportLocationsDatabaseAPITestCase(test.TestCase):
605 604
606 self.assertTrue(actual_result == [initial_location]) 605 self.assertTrue(actual_result == [initial_location])
607 606
607 def test_get_admin_export_locations(self):
608 ctxt_user = context.RequestContext(
609 user_id='fake user', project_id='fake project', is_admin=False)
610 share = db_utils.create_share()
611 locations = [
612 {'path': 'fake1/1/', 'is_admin_only': True},
613 {'path': 'fake2/2/', 'is_admin_only': True},
614 {'path': 'fake3/3/', 'is_admin_only': True},
615 ]
616
617 db_api.share_export_locations_update(
618 self.ctxt, share.instance['id'], locations, delete=False)
619
620 user_result = db_api.share_export_locations_get(ctxt_user, share['id'])
621 self.assertEqual([], user_result)
622
623 admin_result = db_api.share_export_locations_get(
624 self.ctxt, share['id'])
625 self.assertEqual(3, len(admin_result))
626 for location in locations:
627 self.assertIn(location['path'], admin_result)
628
629 def test_get_user_export_locations(self):
630 ctxt_user = context.RequestContext(
631 user_id='fake user', project_id='fake project', is_admin=False)
632 share = db_utils.create_share()
633 locations = [
634 {'path': 'fake1/1/', 'is_admin_only': False},
635 {'path': 'fake2/2/', 'is_admin_only': False},
636 {'path': 'fake3/3/', 'is_admin_only': False},
637 ]
638
639 db_api.share_export_locations_update(
640 self.ctxt, share.instance['id'], locations, delete=False)
641
642 user_result = db_api.share_export_locations_get(ctxt_user, share['id'])
643 self.assertEqual(3, len(user_result))
644 for location in locations:
645 self.assertIn(location['path'], user_result)
646
647 admin_result = db_api.share_export_locations_get(
648 self.ctxt, share['id'])
649 self.assertEqual(3, len(admin_result))
650 for location in locations:
651 self.assertIn(location['path'], admin_result)
652
653 def test_get_user_export_locations_old_view(self):
654 ctxt_user = context.RequestContext(
655 user_id='fake user', project_id='fake project', is_admin=False)
656 share = db_utils.create_share()
657 locations = ['fake1/1/', 'fake2/2', 'fake3/3']
658
659 db_api.share_export_locations_update(
660 self.ctxt, share.instance['id'], locations, delete=False)
661
662 user_result = db_api.share_export_locations_get(ctxt_user, share['id'])
663 self.assertEqual(locations, user_result)
664
665 admin_result = db_api.share_export_locations_get(
666 self.ctxt, share['id'])
667 self.assertEqual(locations, admin_result)
668
669
670@ddt.ddt
671class ShareInstanceExportLocationsMetadataDatabaseAPITestCase(test.TestCase):
672
673 def setUp(self):
674 super(self.__class__, self).setUp()
675 self.ctxt = context.get_admin_context()
676 self.share = db_utils.create_share()
677 self.initial_locations = ['/fake/foo/', '/fake/bar', '/fake/quuz']
678 db_api.share_export_locations_update(
679 self.ctxt, self.share.instance['id'], self.initial_locations,
680 delete=False)
681
682 def _get_export_location_uuid_by_path(self, path):
683 els = db_api.share_export_locations_get_by_share_id(
684 self.ctxt, self.share.id)
685 export_location_uuid = None
686 for el in els:
687 if el.path == path:
688 export_location_uuid = el.uuid
689 self.assertFalse(export_location_uuid is None)
690 return export_location_uuid
691
692 def test_get_export_locations_by_share_id(self):
693 els = db_api.share_export_locations_get_by_share_id(
694 self.ctxt, self.share.id)
695 self.assertEqual(3, len(els))
696 for path in self.initial_locations:
697 self.assertTrue(any([path in el.path for el in els]))
698
699 def test_get_export_locations_by_share_instance_id(self):
700 els = db_api.share_export_locations_get_by_share_instance_id(
701 self.ctxt, self.share.instance.id)
702 self.assertEqual(3, len(els))
703 for path in self.initial_locations:
704 self.assertTrue(any([path in el.path for el in els]))
705
706 def test_export_location_metadata_update_delete(self):
707 export_location_uuid = self._get_export_location_uuid_by_path(
708 self.initial_locations[0])
709 metadata = {
710 'foo_key': 'foo_value',
711 'bar_key': 'bar_value',
712 'quuz_key': 'quuz_value',
713 }
714
715 db_api.export_location_metadata_update(
716 self.ctxt, export_location_uuid, metadata, False)
717
718 db_api.export_location_metadata_delete(
719 self.ctxt, export_location_uuid, list(metadata.keys())[0:-1])
720
721 result = db_api.export_location_metadata_get(
722 self.ctxt, export_location_uuid)
723
724 key = list(metadata.keys())[-1]
725 self.assertEqual({key: metadata[key]}, result)
726
727 db_api.export_location_metadata_delete(
728 self.ctxt, export_location_uuid)
729
730 result = db_api.export_location_metadata_get(
731 self.ctxt, export_location_uuid)
732 self.assertEqual({}, result)
733
734 def test_export_location_metadata_update_get(self):
735
736 # Write metadata for target export location
737 export_location_uuid = self._get_export_location_uuid_by_path(
738 self.initial_locations[0])
739 metadata = {'foo_key': 'foo_value', 'bar_key': 'bar_value'}
740 db_api.export_location_metadata_update(
741 self.ctxt, export_location_uuid, metadata, False)
742
743 # Write metadata for some concurrent export location
744 other_export_location_uuid = self._get_export_location_uuid_by_path(
745 self.initial_locations[1])
746 other_metadata = {'key_from_other_el': 'value_of_key_from_other_el'}
747 db_api.export_location_metadata_update(
748 self.ctxt, other_export_location_uuid, other_metadata, False)
749
750 result = db_api.export_location_metadata_get(
751 self.ctxt, export_location_uuid)
752
753 self.assertEqual(metadata, result)
754
755 updated_metadata = {
756 'foo_key': metadata['foo_key'],
757 'quuz_key': 'quuz_value',
758 }
759
760 db_api.export_location_metadata_update(
761 self.ctxt, export_location_uuid, updated_metadata, True)
762
763 result = db_api.export_location_metadata_get(
764 self.ctxt, export_location_uuid)
765
766 self.assertEqual(updated_metadata, result)
767
768 @ddt.data(
769 ("k", "v"),
770 ("k" * 256, "v"),
771 ("k", "v" * 1024),
772 ("k" * 256, "v" * 1024),
773 )
774 @ddt.unpack
775 def test_set_metadata_with_different_length(self, key, value):
776 export_location_uuid = self._get_export_location_uuid_by_path(
777 self.initial_locations[1])
778 metadata = {key: value}
779
780 db_api.export_location_metadata_update(
781 self.ctxt, export_location_uuid, metadata, False)
782
783 result = db_api.export_location_metadata_get(
784 self.ctxt, export_location_uuid)
785
786 self.assertEqual(metadata, result)
787
608 788
609@ddt.ddt 789@ddt.ddt
610class DriverPrivateDataDatabaseAPITestCase(test.TestCase): 790class DriverPrivateDataDatabaseAPITestCase(test.TestCase):
diff --git a/manila/tests/policy.json b/manila/tests/policy.json
index 3a3f469..2f04348 100644
--- a/manila/tests/policy.json
+++ b/manila/tests/policy.json
@@ -33,6 +33,8 @@
33 "share:unmanage": "rule:admin_api", 33 "share:unmanage": "rule:admin_api",
34 "share:force_delete": "rule:admin_api", 34 "share:force_delete": "rule:admin_api",
35 "share:reset_status": "rule:admin_api", 35 "share:reset_status": "rule:admin_api",
36 "share_export_location:index": "rule:default",
37 "share_export_location:show": "rule:default",
36 38
37 "share_type:index": "rule:default", 39 "share_type:index": "rule:default",
38 "share_type:show": "rule:default", 40 "share_type:show": "rule:default",
@@ -53,6 +55,8 @@
53 "share_instance:show": "rule:admin_api", 55 "share_instance:show": "rule:admin_api",
54 "share_instance:force_delete": "rule:admin_api", 56 "share_instance:force_delete": "rule:admin_api",
55 "share_instance:reset_status": "rule:admin_api", 57 "share_instance:reset_status": "rule:admin_api",
58 "share_instance_export_location:index": "rule:admin_api",
59 "share_instance_export_location:show": "rule:admin_api",
56 60
57 "share_snapshot:force_delete": "rule:admin_api", 61 "share_snapshot:force_delete": "rule:admin_api",
58 "share_snapshot:reset_status": "rule:admin_api", 62 "share_snapshot:reset_status": "rule:admin_api",
diff --git a/manila/tests/share/drivers/test_generic.py b/manila/tests/share/drivers/test_generic.py
index 3b6d52c..825553b 100644
--- a/manila/tests/share/drivers/test_generic.py
+++ b/manila/tests/share/drivers/test_generic.py
@@ -339,11 +339,16 @@ class GenericShareDriverTestCase(test.TestCase):
339 mock.Mock(return_value=volume2)) 339 mock.Mock(return_value=volume2))
340 self.mock_object(self._driver, '_format_device') 340 self.mock_object(self._driver, '_format_device')
341 self.mock_object(self._driver, '_mount_device') 341 self.mock_object(self._driver, '_mount_device')
342 expected_el = {
343 'is_admin_only': False,
344 'path': 'fakelocation',
345 'metadata': {'export_location_metadata_example': 'example'},
346 }
342 347
343 result = self._driver.create_share( 348 result = self._driver.create_share(
344 self._context, self.share, share_server=self.server) 349 self._context, self.share, share_server=self.server)
345 350
346 self.assertEqual('fakelocation', result) 351 self.assertEqual(expected_el, result)
347 self._driver._allocate_container.assert_called_once_with( 352 self._driver._allocate_container.assert_called_once_with(
348 self._driver.admin_context, self.share) 353 self._driver.admin_context, self.share)
349 self._driver._attach_volume.assert_called_once_with( 354 self._driver._attach_volume.assert_called_once_with(
diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py
index 5f3783c..4d9f054 100644
--- a/manila/tests/share/test_api.py
+++ b/manila/tests/share/test_api.py
@@ -1629,10 +1629,32 @@ class ShareAPITestCase(test.TestCase):
1629 share = db_utils.create_share( 1629 share = db_utils.create_share(
1630 status=constants.STATUS_AVAILABLE, 1630 status=constants.STATUS_AVAILABLE,
1631 host='fake@backend#pool', share_type_id='fake_type_id') 1631 host='fake@backend#pool', share_type_id='fake_type_id')
1632 request_spec = {'share_properties': share, 1632 request_spec = {
1633 'share_instance_properties': share.instance.to_dict(), 1633 'share_properties': {
1634 'share_type': 'fake_type', 1634 'size': share['size'],
1635 'share_id': share['id']} 1635 'user_id': share['user_id'],
1636 'project_id': share['project_id'],
1637 'share_server_id': share['share_server_id'],
1638 'snapshot_support': share['snapshot_support'],
1639 'share_proto': share['share_proto'],
1640 'share_type_id': share['share_type_id'],
1641 'is_public': share['is_public'],
1642 'consistency_group_id': share['consistency_group_id'],
1643 'source_cgsnapshot_member_id': share[
1644 'source_cgsnapshot_member_id'],
1645 'snapshot_id': share['snapshot_id'],
1646 },
1647 'share_instance_properties': {
1648 'availability_zone_id': share.instance['availability_zone_id'],
1649 'share_network_id': share.instance['share_network_id'],
1650 'share_server_id': share.instance['share_server_id'],
1651 'share_id': share.instance['share_id'],
1652 'host': share.instance['host'],
1653 'status': share.instance['status'],
1654 },
1655 'share_type': 'fake_type',
1656 'share_id': share['id'],
1657 }
1636 1658
1637 self.mock_object(self.scheduler_rpcapi, 'migrate_share_to_host') 1659 self.mock_object(self.scheduler_rpcapi, 'migrate_share_to_host')
1638 self.mock_object(share_types, 'get_share_type', 1660 self.mock_object(share_types, 'get_share_type',
diff --git a/manila/tests/share/test_manager.py b/manila/tests/share/test_manager.py
index ec0ee29..230d05d 100644
--- a/manila/tests/share/test_manager.py
+++ b/manila/tests/share/test_manager.py
@@ -2473,7 +2473,13 @@ class ShareManagerTestCase(test.TestCase):
2473 status_success = { 2473 status_success = {
2474 'task_state': constants.STATUS_TASK_STATE_MIGRATION_SUCCESS 2474 'task_state': constants.STATUS_TASK_STATE_MIGRATION_SUCCESS
2475 } 2475 }
2476 share_server = 'fake-share-server' 2476 share_server = {
2477 'id': 'fake_share_server_id',
2478 'share_network_id': 'fake_share_network_id',
2479 'host': 'fake_host',
2480 'status': 'fake_status',
2481 'backend_details': {'foo': 'bar'},
2482 }
2477 migration_info = 'fake-info' 2483 migration_info = 'fake-info'
2478 2484
2479 manager = self.share_manager 2485 manager = self.share_manager
@@ -2519,7 +2525,13 @@ class ShareManagerTestCase(test.TestCase):
2519 status_success = { 2525 status_success = {
2520 'task_state': constants.STATUS_TASK_STATE_MIGRATION_SUCCESS 2526 'task_state': constants.STATUS_TASK_STATE_MIGRATION_SUCCESS
2521 } 2527 }
2522 share_server = 'fake-share-server' 2528 share_server = {
2529 'id': 'fake_share_server_id',
2530 'share_network_id': 'fake_share_network_id',
2531 'host': 'fake_host',
2532 'status': 'fake_status',
2533 'backend_details': {'foo': 'bar'},
2534 }
2523 migration_info = 'fake-info' 2535 migration_info = 'fake-info'
2524 2536
2525 manager = self.share_manager 2537 manager = self.share_manager
@@ -2560,7 +2572,13 @@ class ShareManagerTestCase(test.TestCase):
2560 status_success = { 2572 status_success = {
2561 'task_state': constants.STATUS_TASK_STATE_MIGRATION_SUCCESS 2573 'task_state': constants.STATUS_TASK_STATE_MIGRATION_SUCCESS
2562 } 2574 }
2563 share_server = 'fake-share-server' 2575 share_server = {
2576 'id': 'fake_share_server_id',
2577 'share_network_id': 'fake_share_network_id',
2578 'host': 'fake_host',
2579 'status': 'fake_status',
2580 'backend_details': {'foo': 'bar'},
2581 }
2564 migration_info = 'fake-info' 2582 migration_info = 'fake-info'
2565 2583
2566 manager = self.share_manager 2584 manager = self.share_manager
@@ -2605,7 +2623,13 @@ class ShareManagerTestCase(test.TestCase):
2605 status_error = { 2623 status_error = {
2606 'task_state': constants.STATUS_TASK_STATE_MIGRATION_ERROR 2624 'task_state': constants.STATUS_TASK_STATE_MIGRATION_ERROR
2607 } 2625 }
2608 share_server = 'fake-share-server' 2626 share_server = {
2627 'id': 'fake_share_server_id',
2628 'share_network_id': 'fake_share_network_id',
2629 'host': 'fake_host',
2630 'status': 'fake_status',
2631 'backend_details': {'foo': 'bar'},
2632 }
2609 migration_info = 'fake-info' 2633 migration_info = 'fake-info'
2610 2634
2611 manager = self.share_manager 2635 manager = self.share_manager
@@ -2724,8 +2748,20 @@ class ShareManagerTestCase(test.TestCase):
2724 } 2748 }
2725 status_inactive = {'status': constants.STATUS_INACTIVE} 2749 status_inactive = {'status': constants.STATUS_INACTIVE}
2726 status_available = {'status': constants.STATUS_AVAILABLE} 2750 status_available = {'status': constants.STATUS_AVAILABLE}
2727 share_server = 'fake-server' 2751 share_server = {
2728 new_share_server = 'new-fake-server' 2752 'id': 'fake_share_server_id',
2753 'share_network_id': 'fake_share_network_id',
2754 'host': 'fake_host',
2755 'status': 'fake_status',
2756 'backend_details': {'foo': 'bar'},
2757 }
2758 new_share_server = {
2759 'id': 'fake_share_server_id2',
2760 'share_network_id': 'fake_share_network_id2',
2761 'host': 'fake_host2',
2762 'status': 'fake_status2',
2763 'backend_details': {'foo2': 'bar2'},
2764 }
2729 src_migration_info = 'fake-src-migration-info' 2765 src_migration_info = 'fake-src-migration-info'
2730 dest_migration_info = 'fake-dest-migration-info' 2766 dest_migration_info = 'fake-dest-migration-info'
2731 2767
diff --git a/manila/tests/test_exception.py b/manila/tests/test_exception.py
index 431636f..96e38e6 100644
--- a/manila/tests/test_exception.py
+++ b/manila/tests/test_exception.py
@@ -462,6 +462,13 @@ class ManilaExceptionResponseCode404(test.TestCase):
462 self.assertEqual(404, e.code) 462 self.assertEqual(404, e.code)
463 self.assertIn(name, e.msg) 463 self.assertIn(name, e.msg)
464 464
465 def test_export_location_not_found(self):
466 # verify response code for exception.ExportLocationNotFound
467 uuid = "fake-export-location-uuid"
468 e = exception.ExportLocationNotFound(uuid=uuid)
469 self.assertEqual(404, e.code)
470 self.assertIn(uuid, e.msg)
471
465 def test_share_resource_not_found(self): 472 def test_share_resource_not_found(self):
466 # verify response code for exception.ShareResourceNotFound 473 # verify response code for exception.ShareResourceNotFound
467 share_id = "fake_share_id" 474 share_id = "fake_share_id"
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index ddf11cd..fb85fe3 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -36,7 +36,7 @@ ShareGroup = [
36 help="The minimum api microversion is configured to be the " 36 help="The minimum api microversion is configured to be the "
37 "value of the minimum microversion supported by Manila."), 37 "value of the minimum microversion supported by Manila."),
38 cfg.StrOpt("max_api_microversion", 38 cfg.StrOpt("max_api_microversion",
39 default="2.8", 39 default="2.9",
40 help="The maximum api microversion is configured to be the " 40 help="The maximum api microversion is configured to be the "
41 "value of the latest microversion supported by Manila."), 41 "value of the latest microversion supported by Manila."),
42 cfg.StrOpt("region", 42 cfg.StrOpt("region",
diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py
index ff062bf..d017b91 100644
--- a/manila_tempest_tests/services/share/v2/json/shares_client.py
+++ b/manila_tempest_tests/services/share/v2/json/shares_client.py
@@ -238,6 +238,23 @@ class SharesV2Client(shares_client.SharesClient):
238 self.expected_success(200, resp.status) 238 self.expected_success(200, resp.status)
239 return self._parse_resp(body) 239 return self._parse_resp(body)
240 240
241 def get_share_export_location(
242 self, share_id, export_location_uuid, version=LATEST_MICROVERSION):
243 resp, body = self.get(
244 "shares/%(share_id)s/export_locations/%(el_uuid)s" % {
245 "share_id": share_id, "el_uuid": export_location_uuid},
246 version=version)
247 self.expected_success(200, resp.status)
248 return self._parse_resp(body)
249
250 def list_share_export_locations(
251 self, share_id, version=LATEST_MICROVERSION):
252 resp, body = self.get(
253 "shares/%(share_id)s/export_locations" % {"share_id": share_id},
254 version=version)
255 self.expected_success(200, resp.status)
256 return self._parse_resp(body)
257
241 def delete_share(self, share_id, params=None, 258 def delete_share(self, share_id, params=None,
242 version=LATEST_MICROVERSION): 259 version=LATEST_MICROVERSION):
243 uri = "shares/%s" % share_id 260 uri = "shares/%s" % share_id
@@ -265,6 +282,24 @@ class SharesV2Client(shares_client.SharesClient):
265 self.expected_success(200, resp.status) 282 self.expected_success(200, resp.status)
266 return self._parse_resp(body) 283 return self._parse_resp(body)
267 284
285 def get_share_instance_export_location(
286 self, instance_id, export_location_uuid,
287 version=LATEST_MICROVERSION):
288 resp, body = self.get(
289 "share_instances/%(instance_id)s/export_locations/%(el_uuid)s" % {
290 "instance_id": instance_id, "el_uuid": export_location_uuid},
291 version=version)
292 self.expected_success(200, resp.status)
293 return self._parse_resp(body)
294
295 def list_share_instance_export_locations(
296 self, instance_id, version=LATEST_MICROVERSION):
297 resp, body = self.get(
298 "share_instances/%s/export_locations" % instance_id,
299 version=version)
300 self.expected_success(200, resp.status)
301 return self._parse_resp(body)
302
268 def wait_for_share_instance_status(self, instance_id, status, 303 def wait_for_share_instance_status(self, instance_id, status,
269 version=LATEST_MICROVERSION): 304 version=LATEST_MICROVERSION):
270 """Waits for a share to reach a given status.""" 305 """Waits for a share to reach a given status."""
diff --git a/manila_tempest_tests/tests/api/admin/test_export_locations.py b/manila_tempest_tests/tests/api/admin/test_export_locations.py
new file mode 100644
index 0000000..9c759fc
--- /dev/null
+++ b/manila_tempest_tests/tests/api/admin/test_export_locations.py
@@ -0,0 +1,143 @@
1# Copyright 2015 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16from oslo_utils import timeutils
17from oslo_utils import uuidutils
18import six
19from tempest import config
20from tempest import test
21
22from manila_tempest_tests import clients_share as clients
23from manila_tempest_tests.tests.api import base
24
25CONF = config.CONF
26
27
28@base.skip_if_microversion_not_supported("2.9")
29class ExportLocationsTest(base.BaseSharesAdminTest):
30
31 @classmethod
32 def resource_setup(cls):
33 super(ExportLocationsTest, cls).resource_setup()
34 cls.admin_client = cls.shares_v2_client
35 cls.member_client = clients.Manager().shares_v2_client
36 cls.share = cls.create_share()
37 cls.share = cls.shares_v2_client.get_share(cls.share['id'])
38 cls.share_instances = cls.shares_v2_client.get_instances_of_share(
39 cls.share['id'])
40
41 def _verify_export_location_structure(self, export_locations,
42 role='admin'):
43 expected_keys = [
44 'created_at', 'updated_at', 'path', 'uuid',
45 ]
46 if role == 'admin':
47 expected_keys.extend(['is_admin_only', 'share_instance_id'])
48
49 if not isinstance(export_locations, (list, tuple, set)):
50 export_locations = (export_locations, )
51
52 for export_location in export_locations:
53 self.assertEqual(len(expected_keys), len(export_location))
54 for key in expected_keys:
55 self.assertIn(key, export_location)
56 if role == 'admin':
57 self.assertIn(export_location['is_admin_only'], (True, False))
58 self.assertTrue(
59 uuidutils.is_uuid_like(
60 export_location['share_instance_id']))
61 self.assertTrue(uuidutils.is_uuid_like(export_location['uuid']))
62 self.assertTrue(
63 isinstance(export_location['path'], six.string_types))
64 for time in (export_location['created_at'],
65 export_location['updated_at']):
66 # If var 'time' has incorrect value then ValueError exception
67 # is expected to be raised. So, just try parse it making
68 # assertion that it has proper date value.
69 timeutils.parse_strtime(time)
70
71 @test.attr(type=["gate", ])
72 def test_list_share_export_locations(self):
73 export_locations = self.admin_client.list_share_export_locations(
74 self.share['id'])
75
76 self._verify_export_location_structure(export_locations)
77
78 @test.attr(type=["gate", ])
79 def test_get_share_export_location(self):
80 export_locations = self.admin_client.list_share_export_locations(
81 self.share['id'])
82
83 for export_location in export_locations:
84 el = self.admin_client.get_share_export_location(
85 self.share['id'], export_location['uuid'])
86 self._verify_export_location_structure(el)
87
88 @test.attr(type=["gate", ])
89 def test_list_share_export_locations_by_member(self):
90 export_locations = self.member_client.list_share_export_locations(
91 self.share['id'])
92
93 self._verify_export_location_structure(export_locations, 'member')
94
95 @test.attr(type=["gate", ])
96 def test_get_share_export_location_by_member(self):
97 export_locations = self.admin_client.list_share_export_locations(
98 self.share['id'])
99
100 for export_location in export_locations:
101 el = self.member_client.get_share_export_location(
102 self.share['id'], export_location['uuid'])
103 self._verify_export_location_structure(el, 'member')
104
105 @test.attr(type=["gate", ])
106 def test_list_share_instance_export_locations(self):
107 for share_instance in self.share_instances:
108 export_locations = (
109 self.admin_client.list_share_instance_export_locations(
110 share_instance['id']))
111 self._verify_export_location_structure(export_locations)
112
113 @test.attr(type=["gate", ])
114 def test_get_share_instance_export_location(self):
115 for share_instance in self.share_instances:
116 export_locations = (
117 self.admin_client.list_share_instance_export_locations(
118 share_instance['id']))
119 for el in export_locations:
120 el = self.admin_client.get_share_instance_export_location(
121 share_instance['id'], el['uuid'])
122 self._verify_export_location_structure(el)
123
124 @test.attr(type=["gate", ])
125 def test_share_contains_all_export_locations_of_all_share_instances(self):
126 share_export_locations = self.admin_client.list_share_export_locations(
127 self.share['id'])
128 share_instances_export_locations = []
129 for share_instance in self.share_instances:
130 share_instance_export_locations = (
131 self.admin_client.list_share_instance_export_locations(
132 share_instance['id']))
133 share_instances_export_locations.extend(
134 share_instance_export_locations)
135
136 self.assertEqual(
137 len(share_export_locations),
138 len(share_instances_export_locations)
139 )
140 self.assertEqual(
141 sorted(share_export_locations, key=lambda el: el['uuid']),
142 sorted(share_instances_export_locations, key=lambda el: el['uuid'])
143 )
diff --git a/manila_tempest_tests/tests/api/admin/test_export_locations_negative.py b/manila_tempest_tests/tests/api/admin/test_export_locations_negative.py
new file mode 100644
index 0000000..9d53373
--- /dev/null
+++ b/manila_tempest_tests/tests/api/admin/test_export_locations_negative.py
@@ -0,0 +1,94 @@
1# Copyright 2015 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16from tempest import config
17from tempest import test
18from tempest_lib import exceptions as lib_exc
19
20from manila_tempest_tests import clients_share as clients
21from manila_tempest_tests.tests.api import base
22
23CONF = config.CONF
24
25
26@base.skip_if_microversion_not_supported("2.9")
27class ExportLocationsNegativeTest(base.BaseSharesAdminTest):
28
29 @classmethod
30 def resource_setup(cls):
31 super(ExportLocationsNegativeTest, cls).resource_setup()
32 cls.admin_client = cls.shares_v2_client
33 cls.member_client = clients.Manager().shares_v2_client
34 cls.share = cls.create_share()
35 cls.share = cls.shares_v2_client.get_share(cls.share['id'])
36 cls.share_instances = cls.shares_v2_client.get_instances_of_share(
37 cls.share['id'])
38
39 @test.attr(type=["gate", "negative"])
40 def test_get_export_locations_by_inexistent_share(self):
41 self.assertRaises(
42 lib_exc.NotFound,
43 self.admin_client.list_share_export_locations,
44 "fake-inexistent-share-id",
45 )
46
47 @test.attr(type=["gate", "negative"])
48 def test_get_inexistent_share_export_location(self):
49 self.assertRaises(
50 lib_exc.NotFound,
51 self.admin_client.get_share_export_location,
52 self.share['id'],
53 "fake-inexistent-share-instance-id",
54 )
55
56 @test.attr(type=["gate", "negative"])
57 def test_get_export_locations_by_inexistent_share_instance(self):
58 self.assertRaises(
59 lib_exc.NotFound,
60 self.admin_client.list_share_instance_export_locations,
61 "fake-inexistent-share-instance-id",
62 )
63
64 @test.attr(type=["gate", "negative"])
65 def test_get_inexistent_share_instance_export_location(self):
66 for share_instance in self.share_instances:
67 self.assertRaises(
68 lib_exc.NotFound,
69 self.admin_client.get_share_instance_export_location,
70 share_instance['id'],
71 "fake-inexistent-share-instance-id",
72 )
73
74 @test.attr(type=["gate", "negative"])
75 def test_list_share_instance_export_locations_by_member(self):
76 for share_instance in self.share_instances:
77 self.assertRaises(
78 lib_exc.Forbidden,
79 self.member_client.list_share_instance_export_locations,
80 "fake-inexistent-share-instance-id",
81 )
82
83 @test.attr(type=["gate", "negative"])
84 def test_get_share_instance_export_location_by_member(self):
85 for share_instance in self.share_instances:
86 export_locations = (
87 self.admin_client.list_share_instance_export_locations(
88 share_instance['id']))
89 for el in export_locations:
90 self.assertRaises(
91 lib_exc.Forbidden,
92 self.member_client.get_share_instance_export_location,
93 share_instance['id'], el['uuid'],
94 )
diff --git a/manila_tempest_tests/tests/api/admin/test_share_instances.py b/manila_tempest_tests/tests/api/admin/test_share_instances.py
index 1202b9d..c5f96c8 100644
--- a/manila_tempest_tests/tests/api/admin/test_share_instances.py
+++ b/manila_tempest_tests/tests/api/admin/test_share_instances.py
@@ -17,6 +17,7 @@ from tempest import config
17from tempest import test 17from tempest import test
18 18
19from manila_tempest_tests.tests.api import base 19from manila_tempest_tests.tests.api import base
20from manila_tempest_tests import utils
20 21
21CONF = config.CONF 22CONF = config.CONF
22 23
@@ -58,21 +59,31 @@ class ShareInstancesTest(base.BaseSharesAdminTest):
58 msg = 'Share instance for share %s was not found.' % self.share['id'] 59 msg = 'Share instance for share %s was not found.' % self.share['id']
59 self.assertIn(self.share['id'], share_ids, msg) 60 self.assertIn(self.share['id'], share_ids, msg)
60 61
61 @test.attr(type=["gate", ]) 62 def _get_share_instance(self, version):
62 def test_get_share_instance_v2_3(self):
63 """Test that we get the proper keys back for the instance.""" 63 """Test that we get the proper keys back for the instance."""
64 share_instances = self.shares_v2_client.get_instances_of_share( 64 share_instances = self.shares_v2_client.get_instances_of_share(
65 self.share['id'], version='2.3' 65 self.share['id'], version=version,
66 ) 66 )
67 si = self.shares_v2_client.get_share_instance(share_instances[0]['id'], 67 si = self.shares_v2_client.get_share_instance(
68 version='2.3') 68 share_instances[0]['id'], version=version)
69 69
70 expected_keys = ['host', 'share_id', 'id', 'share_network_id', 70 expected_keys = [
71 'status', 'availability_zone', 'share_server_id', 71 'host', 'share_id', 'id', 'share_network_id', 'status',
72 'export_locations', 'export_location', 'created_at'] 72 'availability_zone', 'share_server_id', 'created_at',
73 actual_keys = si.keys() 73 ]
74 self.assertEqual(sorted(expected_keys), sorted(actual_keys), 74 if utils.is_microversion_lt(version, '2.9'):
75 expected_keys.extend(["export_location", "export_locations"])
76 expected_keys = sorted(expected_keys)
77 actual_keys = sorted(si.keys())
78 self.assertEqual(expected_keys, actual_keys,
75 'Share instance %s returned incorrect keys; ' 79 'Share instance %s returned incorrect keys; '
76 'expected %s, got %s.' % (si['id'], 80 'expected %s, got %s.' % (
77 sorted(expected_keys), 81 si['id'], expected_keys, actual_keys))
78 sorted(actual_keys))) 82
83 @test.attr(type=["gate", ])
84 def test_get_share_instance_v2_3(self):
85 self._get_share_instance('2.3')
86
87 @test.attr(type=["gate", ])
88 def test_get_share_instance_v2_9(self):
89 self._get_share_instance('2.9')
diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py
index e2ce55f..f9a678d 100644
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -338,7 +338,8 @@ class BaseSharesTest(test.BaseTestCase):
338 def migrate_share(cls, share_id, dest_host, client=None, **kwargs): 338 def migrate_share(cls, share_id, dest_host, client=None, **kwargs):
339 client = client or cls.shares_v2_client 339 client = client or cls.shares_v2_client
340 client.migrate_share(share_id, dest_host, **kwargs) 340 client.migrate_share(share_id, dest_host, **kwargs)
341 share = client.wait_for_migration_completed(share_id, dest_host) 341 share = client.wait_for_migration_completed(
342 share_id, dest_host, version=kwargs.get('version'))
342 return share 343 return share
343 344
344 @classmethod 345 @classmethod
diff --git a/manila_tempest_tests/tests/api/test_shares.py b/manila_tempest_tests/tests/api/test_shares.py
index 1cf081e..977cfcc 100644
--- a/manila_tempest_tests/tests/api/test_shares.py
+++ b/manila_tempest_tests/tests/api/test_shares.py
@@ -19,6 +19,7 @@ from tempest_lib import exceptions as lib_exc # noqa
19import testtools # noqa 19import testtools # noqa
20 20
21from manila_tempest_tests.tests.api import base 21from manila_tempest_tests.tests.api import base
22from manila_tempest_tests import utils
22 23
23CONF = config.CONF 24CONF = config.CONF
24 25
@@ -40,7 +41,7 @@ class SharesNFSTest(base.BaseSharesTest):
40 41
41 share = self.create_share(self.protocol) 42 share = self.create_share(self.protocol)
42 detailed_elements = {'name', 'id', 'availability_zone', 43 detailed_elements = {'name', 'id', 'availability_zone',
43 'description', 'export_location', 'project_id', 44 'description', 'project_id',
44 'host', 'created_at', 'share_proto', 'metadata', 45 'host', 'created_at', 'share_proto', 'metadata',
45 'size', 'snapshot_id', 'share_network_id', 46 'size', 'snapshot_id', 'share_network_id',
46 'status', 'share_type', 'volume_type', 'links', 47 'status', 'share_type', 'volume_type', 'links',
@@ -57,6 +58,7 @@ class SharesNFSTest(base.BaseSharesTest):
57 58
58 # Get share using v 2.1 - we expect key 'snapshot_support' to be absent 59 # Get share using v 2.1 - we expect key 'snapshot_support' to be absent
59 share_get = self.shares_v2_client.get_share(share['id'], version='2.1') 60 share_get = self.shares_v2_client.get_share(share['id'], version='2.1')
61 detailed_elements.add('export_location')
60 self.assertTrue(detailed_elements.issubset(share_get.keys()), msg) 62 self.assertTrue(detailed_elements.issubset(share_get.keys()), msg)
61 63
62 # Get share using v 2.2 - we expect key 'snapshot_support' to exist 64 # Get share using v 2.2 - we expect key 'snapshot_support' to exist
@@ -64,6 +66,14 @@ class SharesNFSTest(base.BaseSharesTest):
64 detailed_elements.add('snapshot_support') 66 detailed_elements.add('snapshot_support')
65 self.assertTrue(detailed_elements.issubset(share_get.keys()), msg) 67 self.assertTrue(detailed_elements.issubset(share_get.keys()), msg)
66 68
69 if utils.is_microversion_supported('2.9'):
70 # Get share using v 2.9 - key 'export_location' is expected
71 # to be absent
72 share_get = self.shares_v2_client.get_share(
73 share['id'], version='2.9')
74 detailed_elements.remove('export_location')
75 self.assertTrue(detailed_elements.issubset(share_get.keys()), msg)
76
67 # Delete share 77 # Delete share
68 self.shares_v2_client.delete_share(share['id']) 78 self.shares_v2_client.delete_share(share['id'])
69 self.shares_v2_client.wait_for_resource_deletion(share_id=share['id']) 79 self.shares_v2_client.wait_for_resource_deletion(share_id=share['id'])
diff --git a/manila_tempest_tests/tests/api/test_shares_actions.py b/manila_tempest_tests/tests/api/test_shares_actions.py
index 42a7589..f09cc6b 100644
--- a/manila_tempest_tests/tests/api/test_shares_actions.py
+++ b/manila_tempest_tests/tests/api/test_shares_actions.py
@@ -82,11 +82,12 @@ class SharesActionsTest(base.BaseSharesTest):
82 # verify keys 82 # verify keys
83 expected_keys = [ 83 expected_keys = [
84 "status", "description", "links", "availability_zone", 84 "status", "description", "links", "availability_zone",
85 "created_at", "export_location", "project_id", 85 "created_at", "project_id", "volume_type", "share_proto", "name",
86 "export_locations", "volume_type", "share_proto", "name",
87 "snapshot_id", "id", "size", "share_network_id", "metadata", 86 "snapshot_id", "id", "size", "share_network_id", "metadata",
88 "host", "snapshot_id", "is_public", 87 "host", "snapshot_id", "is_public",
89 ] 88 ]
89 if utils.is_microversion_lt(version, '2.9'):
90 expected_keys.extend(["export_location", "export_locations"])
90 if utils.is_microversion_ge(version, '2.2'): 91 if utils.is_microversion_ge(version, '2.2'):
91 expected_keys.append("snapshot_support") 92 expected_keys.append("snapshot_support")
92 if utils.is_microversion_ge(version, '2.4'): 93 if utils.is_microversion_ge(version, '2.4'):
@@ -131,10 +132,15 @@ class SharesActionsTest(base.BaseSharesTest):
131 self._get_share('2.6') 132 self._get_share('2.6')
132 133
133 @test.attr(type=["gate", ]) 134 @test.attr(type=["gate", ])
135 @utils.skip_if_microversion_not_supported('2.9')
136 def test_get_share_export_locations_removed(self):
137 self._get_share('2.9')
138
139 @test.attr(type=["gate", ])
134 def test_list_shares(self): 140 def test_list_shares(self):
135 141
136 # list shares 142 # list shares
137 shares = self.shares_client.list_shares() 143 shares = self.shares_v2_client.list_shares()
138 144
139 # verify keys 145 # verify keys
140 keys = ["name", "id", "links"] 146 keys = ["name", "id", "links"]
@@ -155,11 +161,12 @@ class SharesActionsTest(base.BaseSharesTest):
155 # verify keys 161 # verify keys
156 keys = [ 162 keys = [
157 "status", "description", "links", "availability_zone", 163 "status", "description", "links", "availability_zone",
158 "created_at", "export_location", "project_id", 164 "created_at", "project_id", "volume_type", "share_proto", "name",
159 "export_locations", "volume_type", "share_proto", "name",
160 "snapshot_id", "id", "size", "share_network_id", "metadata", 165 "snapshot_id", "id", "size", "share_network_id", "metadata",
161 "host", "snapshot_id", "is_public", "share_type", 166 "host", "snapshot_id", "is_public", "share_type",
162 ] 167 ]
168 if utils.is_microversion_lt(version, '2.9'):
169 keys.extend(["export_location", "export_locations"])
163 if utils.is_microversion_ge(version, '2.2'): 170 if utils.is_microversion_ge(version, '2.2'):
164 keys.append("snapshot_support") 171 keys.append("snapshot_support")
165 if utils.is_microversion_ge(version, '2.4'): 172 if utils.is_microversion_ge(version, '2.4'):
@@ -195,6 +202,11 @@ class SharesActionsTest(base.BaseSharesTest):
195 self._list_shares_with_detail('2.6') 202 self._list_shares_with_detail('2.6')
196 203
197 @test.attr(type=["gate", ]) 204 @test.attr(type=["gate", ])
205 @utils.skip_if_microversion_not_supported('2.9')
206 def test_list_shares_with_detail_export_locations_removed(self):
207 self._list_shares_with_detail('2.9')
208
209 @test.attr(type=["gate", ])
198 def test_list_shares_with_detail_filter_by_metadata(self): 210 def test_list_shares_with_detail_filter_by_metadata(self):
199 filters = {'metadata': self.metadata} 211 filters = {'metadata': self.metadata}
200 212
diff --git a/manila_tempest_tests/tests/scenario/manager_share.py b/manila_tempest_tests/tests/scenario/manager_share.py
index 51e65ca..3a942e0 100644
--- a/manila_tempest_tests/tests/scenario/manager_share.py
+++ b/manila_tempest_tests/tests/scenario/manager_share.py
@@ -38,6 +38,7 @@ class ShareScenarioTest(manager.NetworkScenarioTest):
38 38
39 # Manila clients 39 # Manila clients
40 cls.shares_client = clients_share.Manager().shares_client 40 cls.shares_client = clients_share.Manager().shares_client
41 cls.shares_v2_client = clients_share.Manager().shares_v2_client
41 cls.shares_admin_client = clients_share.AdminManager().shares_client 42 cls.shares_admin_client = clients_share.AdminManager().shares_client
42 cls.shares_admin_v2_client = ( 43 cls.shares_admin_v2_client = (
43 clients_share.AdminManager().shares_v2_client) 44 clients_share.AdminManager().shares_v2_client)
diff --git a/manila_tempest_tests/tests/scenario/test_share_basic_ops.py b/manila_tempest_tests/tests/scenario/test_share_basic_ops.py
index 7de8870..5373198 100644
--- a/manila_tempest_tests/tests/scenario/test_share_basic_ops.py
+++ b/manila_tempest_tests/tests/scenario/test_share_basic_ops.py
@@ -20,6 +20,7 @@ from tempest_lib.common.utils import data_utils
20from tempest_lib import exceptions 20from tempest_lib import exceptions
21 21
22from manila_tempest_tests.tests.scenario import manager_share as manager 22from manila_tempest_tests.tests.scenario import manager_share as manager
23from manila_tempest_tests import utils
23 24
24CONF = config.CONF 25CONF = config.CONF
25 26
@@ -190,6 +191,9 @@ class ShareBasicOpsBase(manager.ShareScenarioTest):
190 instance1 = self.boot_instance() 191 instance1 = self.boot_instance()
191 self.allow_access_ip(self.share['id'], instance=instance1) 192 self.allow_access_ip(self.share['id'], instance=instance1)
192 ssh_client_inst1 = self.init_ssh(instance1) 193 ssh_client_inst1 = self.init_ssh(instance1)
194
195 # TODO(vponomaryov): use separate API for getting export location for
196 # share when "v2" client is used.
193 first_location = self.share['export_locations'][0] 197 first_location = self.share['export_locations'][0]
194 self.mount_share(first_location, ssh_client_inst1) 198 self.mount_share(first_location, ssh_client_inst1)
195 self.addCleanup(self.umount_share, 199 self.addCleanup(self.umount_share,
@@ -235,12 +239,13 @@ class ShareBasicOpsBase(manager.ShareScenarioTest):
235 239
236 dest_pool = dest_pool['name'] 240 dest_pool = dest_pool['name']
237 241
238 old_export_location = share['export_locations'][0]
239
240 instance1 = self.boot_instance() 242 instance1 = self.boot_instance()
241 self.allow_access_ip(self.share['id'], instance=instance1, 243 self.allow_access_ip(self.share['id'], instance=instance1,
242 cleanup=False) 244 cleanup=False)
243 ssh_client = self.init_ssh(instance1) 245 ssh_client = self.init_ssh(instance1)
246
247 # TODO(vponomaryov): use separate API for getting export location for
248 # share when "v2" client is used.
244 first_location = self.share['export_locations'][0] 249 first_location = self.share['export_locations'][0]
245 self.mount_share(first_location, ssh_client) 250 self.mount_share(first_location, ssh_client)
246 251
@@ -266,12 +271,19 @@ class ShareBasicOpsBase(manager.ShareScenarioTest):
266 self.umount_share(ssh_client) 271 self.umount_share(ssh_client)
267 272
268 share = self.migrate_share(share['id'], dest_pool) 273 share = self.migrate_share(share['id'], dest_pool)
274 if utils.is_microversion_supported("2.9"):
275 second_location = (
276 self.shares_v2_client.list_share_export_locations(
277 share['id'])[0]['path'])
278 else:
279 # NOTE(vponomaryov): following approach is valid for picking up
280 # export location only using microversions lower than '2.9'.
281 second_location = share['export_locations'][0]
269 282
270 self.assertEqual(dest_pool, share['host']) 283 self.assertEqual(dest_pool, share['host'])
271 self.assertNotEqual(old_export_location, share['export_locations'][0]) 284 self.assertNotEqual(first_location, second_location)
272 self.assertEqual('migration_success', share['task_state']) 285 self.assertEqual('migration_success', share['task_state'])
273 286
274 second_location = share['export_locations'][0]
275 self.mount_share(second_location, ssh_client) 287 self.mount_share(second_location, ssh_client)
276 288
277 output = ssh_client.exec_command("ls -lRA --ignore=lost+found /mnt") 289 output = ssh_client.exec_command("ls -lRA --ignore=lost+found /mnt")
diff --git a/releasenotes/notes/add-export-locations-api-6fc6086c6a081faa.yaml b/releasenotes/notes/add-export-locations-api-6fc6086c6a081faa.yaml
new file mode 100644
index 0000000..08ef0d0
--- /dev/null
+++ b/releasenotes/notes/add-export-locations-api-6fc6086c6a081faa.yaml
@@ -0,0 +1,6 @@
1---
2features:
3 - Added APIs for listing export locations per share and share instances.
4deprecations:
5 - Removed 'export_location' and 'export_locations' attributes from share
6 and share instance views starting with microversion '2.9'.