Browse Source

Merge "Implement export location metadata feature"

Jenkins 3 years ago
parent
commit
ea8276cd10
39 changed files with 1839 additions and 128 deletions
  1. 4
    0
      etc/manila/policy.json
  2. 2
    1
      manila/api/openstack/api_version_request.py
  3. 5
    0
      manila/api/openstack/rest_api_version_history.rst
  4. 33
    0
      manila/api/v2/router.py
  5. 78
    0
      manila/api/v2/share_export_locations.py
  6. 67
    0
      manila/api/v2/share_instance_export_locations.py
  7. 71
    0
      manila/api/views/export_locations.py
  8. 11
    1
      manila/api/views/share_instance.py
  9. 6
    0
      manila/api/views/shares.py
  10. 45
    4
      manila/db/api.py
  11. 120
    0
      manila/db/migrations/alembic/versions/dda6de06349_add_export_locations_metadata.py
  12. 254
    69
      manila/db/sqlalchemy/api.py
  13. 40
    1
      manila/db/sqlalchemy/models.py
  14. 4
    0
      manila/exception.py
  15. 50
    8
      manila/share/api.py
  16. 9
    1
      manila/share/drivers/generic.py
  17. 14
    0
      manila/share/manager.py
  18. 152
    0
      manila/tests/api/v2/test_share_export_locations.py
  19. 121
    0
      manila/tests/api/v2/test_share_instance_export_locations.py
  20. 41
    4
      manila/tests/api/v2/test_share_instances.py
  21. 13
    0
      manila/tests/api/v2/test_shares.py
  22. 81
    0
      manila/tests/db/migrations/alembic/migrations_data_checks.py
  23. 182
    2
      manila/tests/db/sqlalchemy/test_api.py
  24. 4
    0
      manila/tests/policy.json
  25. 6
    1
      manila/tests/share/drivers/test_generic.py
  26. 26
    4
      manila/tests/share/test_api.py
  27. 42
    6
      manila/tests/share/test_manager.py
  28. 7
    0
      manila/tests/test_exception.py
  29. 1
    1
      manila_tempest_tests/config.py
  30. 35
    0
      manila_tempest_tests/services/share/v2/json/shares_client.py
  31. 143
    0
      manila_tempest_tests/tests/api/admin/test_export_locations.py
  32. 94
    0
      manila_tempest_tests/tests/api/admin/test_export_locations_negative.py
  33. 25
    14
      manila_tempest_tests/tests/api/admin/test_share_instances.py
  34. 2
    1
      manila_tempest_tests/tests/api/base.py
  35. 11
    1
      manila_tempest_tests/tests/api/test_shares.py
  36. 17
    5
      manila_tempest_tests/tests/api/test_shares_actions.py
  37. 1
    0
      manila_tempest_tests/tests/scenario/manager_share.py
  38. 16
    4
      manila_tempest_tests/tests/scenario/test_share_basic_ops.py
  39. 6
    0
      releasenotes/notes/add-export-locations-api-6fc6086c6a081faa.yaml

+ 4
- 0
etc/manila/policy.json View File

@@ -37,11 +37,15 @@
37 37
     "share:unmanage": "rule:admin_api",
38 38
     "share:force_delete": "rule:admin_api",
39 39
     "share:reset_status": "rule:admin_api",
40
+    "share_export_location:index": "rule:default",
41
+    "share_export_location:show": "rule:default",
40 42
 
41 43
     "share_instance:index": "rule:admin_api",
42 44
     "share_instance:show": "rule:admin_api",
43 45
     "share_instance:force_delete": "rule:admin_api",
44 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 50
     "share_snapshot:create_snapshot": "rule:default",
47 51
     "share_snapshot:delete_snapshot": "rule:default",

+ 2
- 1
manila/api/openstack/api_version_request.py View File

@@ -54,13 +54,14 @@ REST_API_VERSION_HISTORY = """
54 54
     * 2.6 - Return share_type UUID instead of name in Share API
55 55
     * 2.7 - Rename old extension-like API URLs to core-API-like
56 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 60
 # The minimum and maximum versions of the API supported
60 61
 # The default api version request is defined to be the
61 62
 # the minimum version of the API supported.
62 63
 _MIN_API_VERSION = "2.0"
63
-_MAX_API_VERSION = "2.8"
64
+_MAX_API_VERSION = "2.9"
64 65
 DEFAULT_API_VERSION = _MIN_API_VERSION
65 66
 
66 67
 

+ 5
- 0
manila/api/openstack/rest_api_version_history.rst View File

@@ -69,3 +69,8 @@ user documentation.
69 69
 2.8
70 70
 ---
71 71
   Allow to set share visibility explicitly using "manage" API.
72
+
73
+2.9
74
+---
75
+  Add export locations API. Remove export locations from "shares" and
76
+  "share instances" APIs.

+ 33
- 0
manila/api/v2/router.py View File

@@ -38,6 +38,8 @@ from manila.api.v2 import consistency_groups
38 38
 from manila.api.v2 import quota_class_sets
39 39
 from manila.api.v2 import quota_sets
40 40
 from manila.api.v2 import services
41
+from manila.api.v2 import share_export_locations
42
+from manila.api.v2 import share_instance_export_locations
41 43
 from manila.api.v2 import share_instances
42 44
 from manila.api.v2 import share_types
43 45
 from manila.api.v2 import shares
@@ -153,12 +155,43 @@ class APIRouter(manila.api.openstack.APIRouter):
153 155
                         collection={"detail": "GET"},
154 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 175
         mapper.connect("share_instance",
157 176
                        "/{project_id}/shares/{share_id}/instances",
158 177
                        controller=self.resources["share_instances"],
159 178
                        action="get_share_instances",
160 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 195
         self.resources["snapshots"] = share_snapshots.create_resource()
163 196
         mapper.resource("snapshot", "snapshots",
164 197
                         controller=self.resources["snapshots"],

+ 78
- 0
manila/api/v2/share_export_locations.py View File

@@ -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
+
16
+from webob import exc
17
+
18
+from manila.api.openstack import wsgi
19
+from manila.api.views import export_locations as export_locations_views
20
+from manila.db import api as db_api
21
+from manila import exception
22
+from manila.i18n import _
23
+
24
+
25
+class 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
+
77
+def create_resource():
78
+    return wsgi.Resource(ShareExportLocationController())

+ 67
- 0
manila/api/v2/share_instance_export_locations.py View File

@@ -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
+
16
+import six
17
+from webob import exc
18
+
19
+from manila.api.openstack import wsgi
20
+from manila.api.views import export_locations as export_locations_views
21
+from manila.db import api as db_api
22
+from manila import exception
23
+from manila.i18n import _
24
+
25
+
26
+class 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
+
66
+def create_resource():
67
+    return wsgi.Resource(ShareInstanceExportLocationController())

+ 71
- 0
manila/api/views/export_locations.py View File

@@ -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
+
16
+from manila.api import common
17
+
18
+
19
+class 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)

+ 11
- 1
manila/api/views/share_instance.py View File

@@ -18,6 +18,10 @@ class ViewBuilder(common.ViewBuilder):
18 18
 
19 19
     _collection_name = 'share_instances'
20 20
 
21
+    _detail_version_modifiers = [
22
+        "remove_export_locations",
23
+    ]
24
+
21 25
     def detail_list(self, request, instances):
22 26
         """Detailed view of a list of share instances."""
23 27
         return self._list_view(self.detail, request, instances)
@@ -38,7 +42,8 @@ class ViewBuilder(common.ViewBuilder):
38 42
             'export_location': share_instance.get('export_location'),
39 43
             'export_locations': export_locations,
40 44
         }
41
-
45
+        self.update_versioned_resource_dict(
46
+            request, instance_dict, share_instance)
42 47
         return {'share_instance': instance_dict}
43 48
 
44 49
     def _list_view(self, func, request, instances):
@@ -54,3 +59,8 @@ class ViewBuilder(common.ViewBuilder):
54 59
             instances_dict[self._collection_name] = instances_links
55 60
 
56 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')

+ 6
- 0
manila/api/views/shares.py View File

@@ -25,6 +25,7 @@ class ViewBuilder(common.ViewBuilder):
25 25
         "add_consistency_group_fields",
26 26
         "add_task_state_field",
27 27
         "modify_share_type_field",
28
+        "remove_export_locations",
28 29
     ]
29 30
 
30 31
     def summary_list(self, request, shares):
@@ -117,6 +118,11 @@ class ViewBuilder(common.ViewBuilder):
117 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 126
     def _list_view(self, func, request, shares):
121 127
         """Provide a view for a list of shares."""
122 128
         shares_list = [func(request, share)['share'] for share in shares]

+ 45
- 4
manila/db/api.py View File

@@ -597,17 +597,58 @@ def share_metadata_update(context, share, metadata, delete):
597 597
 
598 598
 ###################
599 599
 
600
+def 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
+
600 606
 def share_export_locations_get(context, share_id):
601
-    """Get all exports_locations of share."""
607
+    """Get all export locations of a share."""
602 608
     return IMPL.share_export_locations_get(context, share_id)
603 609
 
604 610
 
611
+def 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
+
618
+def 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
+
605 625
 def share_export_locations_update(context, share_instance_id, export_locations,
606 626
                                   delete=True):
607
-    """Update export locations of share."""
608
-    return IMPL.share_export_locations_update(context, share_instance_id,
609
-                                              export_locations, delete)
627
+    """Update export locations of a share instance."""
628
+    return IMPL.share_export_locations_update(
629
+        context, share_instance_id, export_locations, delete)
630
+
631
+
632
+####################
633
+
634
+def 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
+
640
+def 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
 
647
+def 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
 

+ 120
- 0
manila/db/migrations/alembic/versions/dda6de06349_add_export_locations_metadata.py View File

@@ -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
+
18
+Revision ID: dda6de06349
19
+Revises: 323840a08dc4
20
+Create Date: 2015-11-30 13:50:15.914232
21
+
22
+"""
23
+
24
+# revision identifiers, used by Alembic.
25
+revision = 'dda6de06349'
26
+down_revision = '323840a08dc4'
27
+
28
+from alembic import op
29
+from oslo_log import log
30
+from oslo_utils import uuidutils
31
+import sqlalchemy as sa
32
+
33
+from manila.i18n import _LE
34
+
35
+SI_TABLE_NAME = 'share_instances'
36
+EL_TABLE_NAME = 'share_instance_export_locations'
37
+ELM_TABLE_NAME = 'share_instance_export_locations_metadata'
38
+LOG = log.getLogger(__name__)
39
+
40
+
41
+def 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
+
108
+def 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

+ 254
- 69
manila/db/sqlalchemy/api.py View File

@@ -179,6 +179,20 @@ def require_share_exists(f):
179 179
     return wrapper
180 180
 
181 181
 
182
+def 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
+
182 196
 def model_query(context, model, *args, **kwargs):
183 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 1185
                        with_share_data=False):
1172 1186
     if session is None:
1173 1187
         session = get_session()
1174
-    result = (
1175
-        model_query(context, models.ShareInstance, session=session).filter_by(
1176
-            id=share_instance_id).first()
1177
-    )
1188
+    result = model_query(
1189
+        context, models.ShareInstance, session=session,
1190
+    ).filter_by(
1191
+        id=share_instance_id,
1192
+    ).options(
1193
+        joinedload('export_locations'),
1194
+    ).first()
1178 1195
     if result is None:
1179 1196
         raise exception.NotFound()
1180 1197
 
@@ -1188,10 +1205,11 @@ def share_instance_get(context, share_instance_id, session=None,
1188 1205
 @require_admin_context
1189 1206
 def share_instances_get_all(context):
1190 1207
     session = get_session()
1191
-    return (
1192
-        model_query(context, models.ShareInstance, session=session,
1193
-                    read_deleted="no").all()
1194
-    )
1208
+    return model_query(
1209
+        context, models.ShareInstance, session=session, read_deleted="no",
1210
+    ).options(
1211
+        joinedload('export_locations'),
1212
+    ).all()
1195 1213
 
1196 1214
 
1197 1215
 @require_context
@@ -1200,15 +1218,11 @@ def share_instance_delete(context, instance_id, session=None):
1200 1218
         session = get_session()
1201 1219
 
1202 1220
     with session.begin():
1221
+        share_export_locations_update(context, instance_id, [], delete=True)
1203 1222
         instance_ref = share_instance_get(context, instance_id,
1204 1223
                                           session=session)
1205 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 1225
         share = share_get(context, instance_ref['share_id'], session=session)
1211
-
1212 1226
         if len(share.instances) == 0:
1213 1227
             share.soft_delete(session=session)
1214 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 2033
     return result
2020 2034
 
2021 2035
 
2022
-#################################
2036
+############################
2037
+# Export locations functions
2038
+############################
2039
+
2040
+def _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
2068
+def 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
2079
+def 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 2086
 @require_context
2025 2087
 @require_share_exists
2026 2088
 def 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 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 2096
     return [location['path'] for location in rows]
2031 2097
 
2032 2098
 
2033
-def _share_export_locations_get(context, share_instance_id, session=None):
2034
-    if not session:
2035
-        session = get_session()
2099
+@require_context
2100
+def share_export_location_get_by_uuid(context, export_location_uuid,
2101
+                                      session=None):
2102
+    session = session or get_session()
2036 2103
 
2037
-    return (
2038
-        model_query(context, models.ShareInstanceExportLocations,
2039
-                    session=session, read_deleted="no").
2040
-        filter_by(share_instance_id=share_instance_id).
2041
-        order_by("updated_at").
2042
-        all()
2104
+    query = model_query(
2105
+        context,
2106
+        models.ShareInstanceExportLocations,
2107
+        session=session,
2108
+        read_deleted="no",
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 2121
 @require_context
2047 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 2124
                                   delete):
2050 2125
     # NOTE(u_glide):
2051 2126
     # Backward compatibility code for drivers,
2052
-    # which returns single export_location as string
2053
-    if not isinstance(export_locations, list):
2054
-        export_locations = [export_locations]
2127
+    # which return single export_location as string
2128
+    if not isinstance(export_locations, (list, tuple, set)):
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 2151
     session = get_session()
2057 2152
 
2058
-    with session.begin():
2059
-        location_rows = _share_export_locations_get(
2060
-            context, share_instance_id, session=session)
2153
+    current_el_rows = _share_export_locations_get(
2154
+        context, share_instance_id, session=session)
2061 2155
 
2062
-        def get_path_list_from_rows(rows):
2063
-            return set([l['path'] for l in rows])
2156
+    def get_path_list_from_rows(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):
2068
-            base = timeutils.utcnow()
2069
-            return {
2070
-                # NOTE(u_glide): Incrementing timestamp by microseconds to make
2071
-                # timestamp order match index order.
2072
-                key: base + datetime.timedelta(microseconds=index)
2073
-                for index, key in enumerate(key_list)
2074
-            }
2161
+    def create_indexed_time_dict(key_list):
2162
+        base = timeutils.utcnow()
2163
+        return {
2164
+            # NOTE(u_glide): Incrementing timestamp by microseconds to make
2165
+            # timestamp order match index order.
2166
+            key: base + datetime.timedelta(microseconds=index)
2167
+            for index, key in enumerate(key_list)
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:
2079
-            if delete and location['path'] not in export_locations:
2080
-                location.soft_delete(session)
2081
-            else:
2082
-                updated_at = indexed_update_time[location['path']]
2083
-                location.update({
2084
-                    'updated_at': updated_at,
2085
-                    'deleted': 0,
2086
-                })
2172
+    for el in current_el_rows:
2173
+        if delete and el['path'] not in export_locations_paths:
2174
+            export_location_metadata_delete(context, el['uuid'])
2175
+            el.soft_delete(session)
2176
+        else:
2177
+            updated_at = indexed_update_time[el['path']]
2178
+            el.update({
2179
+                'updated_at': updated_at,
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
2091
-        for path in export_locations:
2092
-            if path in current_locations:
2093
-                # Already updated
2094
-                continue
2208
+    return get_path_list_from_rows(_share_export_locations_get(
2209
+        context, share_instance_id, session=session))
2095 2210
 
2096
-            location_ref = models.ShareInstanceExportLocations()
2097
-            location_ref.update({
2098
-                'path': path,
2099
-                'share_instance_id': share_instance_id,
2100
-                'updated_at': indexed_update_time[path],
2101
-                'deleted': 0,
2211
+
2212
+#####################################
2213
+# Export locations metadata functions
2214
+#####################################
2215
+
2216
+def _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
2231
+def 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
2241
+def 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)
2258
+def 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:
2106
-            return export_locations
2291
+        meta_ref.update(item)
2292
+        meta_ref.save(session=session)
2107 2293
 
2108
-        return get_path_list_from_rows(_share_export_locations_get(
2109
-            context, share_instance_id, session=session))
2294
+    return metadata
2110 2295
 
2111 2296
 
2112
-#################################
2297
+###################################
2113 2298
 
2114 2299
 
2115 2300
 @require_context

+ 40
- 1
manila/db/sqlalchemy/models.py View File

@@ -363,13 +363,52 @@ class ShareInstance(BASE, ManilaBase):
363 363
 
364 364
 
365 365
 class ShareInstanceExportLocations(BASE, ManilaBase):
366
-    """Represents export locations of shares."""
366
+    """Represents export locations of share instances."""
367 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 378
     id = Column(Integer, primary_key=True)
379
+    uuid = Column(String(36), nullable=False, unique=True)
370 380
     share_instance_id = Column(
371 381
         String(36), ForeignKey('share_instances.id'), nullable=False)
372 382
     path = Column(String(2000))
383
+    is_admin_only = Column(Boolean, default=False, nullable=False)
384
+
385
+
386
+class 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
 
375 414
 class ShareTypes(BASE, ManilaBase):

+ 4
- 0
manila/exception.py View File

@@ -422,6 +422,10 @@ class ShareBackendException(ManilaException):
422 422
     message = _("Share backend error: %(msg)s.")
423 423
 
424 424
 
425
+class ExportLocationNotFound(NotFound):
426
+    message = _("Export location %(uuid)s could not be found.")
427
+
428
+
425 429
 class ShareSnapshotNotFound(NotFound):
426 430
     message = _("Snapshot %(snapshot_id)s could not be found.")
427 431
 

+ 50
- 8
manila/share/api.py View File

@@ -289,10 +289,29 @@ class API(base.Base):
289 289
             # NOTE(ameade): Do not cast to driver if creating from cgsnapshot
290 290
             return
291 291
 
292
-        share_dict = share.to_dict()
293
-        share_dict.update(
294
-            {'metadata': self.db.share_metadata_get(context, share['id'])}
295
-        )
292
+        share_properties = {
293
+            'size': share['size'],
294
+            'user_id': share['user_id'],
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 316
         share_type = None
298 317
         if share['share_type_id']:
@@ -300,8 +319,8 @@ class API(base.Base):
300 319
                 context, share['share_type_id'])
301 320
 
302 321
         request_spec = {
303
-            'share_properties': share_dict,
304
-            'share_instance_properties': share_instance.to_dict(),
322
+            'share_properties': share_properties,
323
+            'share_instance_properties': share_instance_properties,
305 324
             'share_proto': share['share_proto'],
306 325
             'share_id': share['id'],
307 326
             'snapshot_id': share['snapshot_id'],
@@ -599,8 +618,31 @@ class API(base.Base):
599 618
         share_type_id = share['share_type_id']
600 619
         if share_type_id:
601 620
             share_type = share_types.get_share_type(context, share_type_id)
602
-        request_spec = {'share_properties': share,
603
-                        'share_instance_properties': share_instance.to_dict(),
621
+
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 646
                         'share_type': share_type,
605 647
                         'share_id': share['id']}
606 648
 

+ 9
- 1
manila/share/drivers/generic.py View File

@@ -242,7 +242,15 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
242 242
         location = helper.create_export(
243 243
             server_details,
244 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 255
     def _format_device(self, server_details, volume):
248 256
         """Formats device attached to the service vm."""

+ 14
- 0
manila/share/manager.py View File

@@ -573,6 +573,13 @@ class ShareManager(manager.SchedulerDependentManager):
573 573
 
574 574
                 share_server = self._get_share_server(ctxt.elevated(),
575 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 584
                 dest_driver_migration_info = rpcapi.get_driver_migration_info(
578 585
                     ctxt, share_instance, share_server)
@@ -663,6 +670,13 @@ class ShareManager(manager.SchedulerDependentManager):
663 670
                                                   share_instance)
664 671
             new_share_server = self._get_share_server(context.elevated(),
665 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 681
             src_migration_info = self.driver.get_migration_info(
668 682
                 context, share_instance, share_server)

+ 152
- 0
manila/tests/api/v2/test_share_export_locations.py View File

@@ -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
+
16
+import ddt
17
+import mock
18
+from webob import exc
19
+
20
+from manila.api.v2 import share_export_locations as export_locations
21
+from manila import context
22
+from manila import db
23
+from manila import exception
24
+from manila import policy
25
+from manila import test
26
+from manila.tests.api import fakes
27
+from manila.tests import db_utils
28
+
29
+
30
+@ddt.ddt
31
+class 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
+        )

+ 121
- 0
manila/tests/api/v2/test_share_instance_export_locations.py View File

@@ -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
+
16
+import ddt
17
+import mock
18
+from webob import exc
19
+
20
+from manila.api.v2 import share_instance_export_locations as export_locations
21
+from manila import context
22
+from manila import db
23
+from manila import exception
24
+from manila import policy
25
+from manila import test
26
+from manila.tests.api import fakes
27
+from manila.tests import db_utils
28
+
29
+
30
+@ddt.ddt
31
+class 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
+        )

+ 41
- 4
manila/tests/api/v2/test_share_instances.py View File

@@ -17,6 +17,7 @@ from oslo_serialization import jsonutils
17 17
 import six
18 18
 from webob import exc as webob_exc
19 19
 
20
+from manila.api.openstack import api_version_request
20 21
 from manila.api.v2 import share_instances
21 22
 from manila.common import constants
22 23
 from manila import context
@@ -56,10 +57,10 @@ class ShareInstancesAPITest(test.TestCase):
56 57
             version=version)
57 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 61
         if context is None:
61 62
             context = self.admin_context
62
-        req = fakes.HTTPRequest.blank('/shares', version="2.3")
63
+        req = fakes.HTTPRequest.blank('/shares', version=version)
63 64
         req.environ['manila.context'] = context
64 65
         return req
65 66
 
@@ -94,10 +95,37 @@ class ShareInstancesAPITest(test.TestCase):
94 95
         self.mock_policy_check.assert_called_once_with(
95 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 126
         test_share = db_utils.create_share(size=1)
99 127
         id = test_share['id']
100
-        req = self._get_request('fake')
128
+        req = self._get_request('fake', version=version)
101 129
         req_context = req.environ['manila.context']
102 130
         share_policy_check_call = mock.call(
103 131
             req_context, 'share', 'get', mock.ANY)
@@ -110,6 +138,15 @@ class ShareInstancesAPITest(test.TestCase):
110 138
             [test_share.instance],
111 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 150
         self.mock_policy_check.assert_has_calls([
114 151
             get_instances_policy_check_call, share_policy_check_call])
115 152
 

+ 13
- 0
manila/tests/api/v2/test_shares.py View File

@@ -814,6 +814,19 @@ class ShareAPITest(test.TestCase):
814 814
         expected['shares'][0]['task_state'] = None
815 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 830
     def test_remove_invalid_options(self):
818 831
         ctx = context.RequestContext('fakeuser', 'fakeproject', is_admin=False)
819 832
         search_opts = {'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}

+ 81
- 0
manila/tests/db/migrations/alembic/migrations_data_checks.py View File

@@ -37,6 +37,7 @@ import abc
37 37
 
38 38
 from oslo_utils import uuidutils
39 39
 import six
40
+from sqlalchemy import exc as sa_exc
40 41
 
41 42
 from manila.db.migrations import utils
42 43
 
@@ -172,3 +173,83 @@ class AvailabilityZoneMigrationChecks(BaseMigrationChecks):
172 173
             self.test_case.assertIn(
173 174
                 service.availability_zone, self.valid_az_names
174 175
             )
176
+
177
+
178
+@map_to_migration('dda6de06349')
179
+class 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)

+ 182
- 2
manila/tests/db/sqlalchemy/test_api.py View File

@@ -573,8 +573,7 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
573 573
 class ShareExportLocationsDatabaseAPITestCase(test.TestCase):
574 574
 
575 575
     def setUp(self):
576
-        """Run before each test."""
577
-        super(ShareExportLocationsDatabaseAPITestCase, self).setUp()
576
+        super(self.__class__, self).setUp()
578 577
         self.ctxt = context.get_admin_context()
579 578
 
580 579
     def test_update_valid_order(self):
@@ -605,6 +604,187 @@ class ShareExportLocationsDatabaseAPITestCase(test.TestCase):
605 604
 
606 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
671
+class 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 789
 @ddt.ddt
610 790
 class DriverPrivateDataDatabaseAPITestCase(test.TestCase):

+ 4
- 0
manila/tests/policy.json View File

@@ -33,6 +33,8 @@
33 33
     "share:unmanage": "rule:admin_api",
34 34
     "share:force_delete": "rule:admin_api",
35 35
     "share:reset_status": "rule:admin_api",
36
+    "share_export_location:index": "rule:default",
37
+    "share_export_location:show": "rule:default",
36 38
 
37 39
     "share_type:index": "rule:default",
38 40
     "share_type:show": "rule:default",
@@ -53,6 +55,8 @@
53 55
     "share_instance:show": "rule:admin_api",
54 56
     "share_instance:force_delete": "rule:admin_api",
55 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 61
     "share_snapshot:force_delete": "rule:admin_api",
58 62
     "share_snapshot:reset_status": "rule:admin_api",

+ 6
- 1
manila/tests/share/drivers/test_generic.py View File

@@ -339,11 +339,16 @@ class GenericShareDriverTestCase(test.TestCase):
339 339
                          mock.Mock(return_value=volume2))
340 340
         self.mock_object(self._driver, '_format_device')
341 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 348
         result = self._driver.create_share(
344 349
             self._context, self.share, share_server=self.server)
345 350
 
346
-        self.assertEqual('fakelocation', result)
351
+        self.assertEqual(expected_el, result)
347 352
         self._driver._allocate_container.assert_called_once_with(
348 353
             self._driver.admin_context, self.share)
349 354
         self._driver._attach_volume.assert_called_once_with(

+ 26
- 4
manila/tests/share/test_api.py View File

@@ -1629,10 +1629,32 @@ class ShareAPITestCase(test.TestCase):
1629 1629
         share = db_utils.create_share(
1630 1630
             status=constants.STATUS_AVAILABLE,
1631 1631
             host='fake@backend#pool', share_type_id='fake_type_id')
1632
-        request_spec = {'share_properties': share,
1633
-                        'share_instance_properties': share.instance.to_dict(),
1634
-                        'share_type': 'fake_type',
1635
-                        'share_id': share['id']}
1632
+        request_spec = {
1633
+            'share_properties': {
1634
+                'size': share['size'],
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 1659
         self.mock_object(self.scheduler_rpcapi, 'migrate_share_to_host')
1638 1660
         self.mock_object(share_types, 'get_share_type',

+ 42
- 6
manila/tests/share/test_manager.py View File

@@ -2473,7 +2473,13 @@ class ShareManagerTestCase(test.TestCase):
2473 2473
         status_success = {
2474 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 2483
         migration_info = 'fake-info'
2478 2484
 
2479 2485
         manager = self.share_manager
@@ -2519,7 +2525,13 @@ class ShareManagerTestCase(test.TestCase):
2519 2525
         status_success = {
2520 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 2535
         migration_info = 'fake-info'
2524 2536
 
2525 2537
         manager = self.share_manager
@@ -2560,7 +2572,13 @@ class ShareManagerTestCase(test.TestCase):
2560 2572
         status_success = {
2561 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 2582
         migration_info = 'fake-info'
2565 2583
 
2566 2584
         manager = self.share_manager
@@ -2605,7 +2623,13 @@ class ShareManagerTestCase(test.TestCase):
2605 2623
         status_error = {
2606 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 2633
         migration_info = 'fake-info'
2610 2634
 
2611 2635
         manager = self.share_manager
@@ -2724,8 +2748,20 @@ class ShareManagerTestCase(test.TestCase):
2724 2748
         }
2725 2749
         status_inactive = {'status': constants.STATUS_INACTIVE}
2726 2750
         status_available = {'status': constants.STATUS_AVAILABLE}
2727
-        share_server = 'fake-server'
2728
-        new_share_server = 'new-fake-server'
2751
+        share_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 2765
         src_migration_info = 'fake-src-migration-info'
2730 2766
         dest_migration_info = 'fake-dest-migration-info'
2731 2767
 

+ 7
- 0
manila/tests/test_exception.py View File

@@ -462,6 +462,13 @@ class ManilaExceptionResponseCode404(test.TestCase):
462 462
         self.assertEqual(404, e.code)
463 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 472
     def test_share_resource_not_found(self):
466 473
         # verify response code for exception.ShareResourceNotFound
467 474
         share_id = "fake_share_id"

+ 1
- 1
manila_tempest_tests/config.py View File

@@ -36,7 +36,7 @@ ShareGroup = [
36 36
                help="The minimum api microversion is configured to be the "
37 37
                     "value of the minimum microversion supported by Manila."),
38 38
     cfg.StrOpt("max_api_microversion",
39
-               default="2.8",
39
+               default="2.9",
40 40
                help="The maximum api microversion is configured to be the "
41 41
                     "value of the latest microversion supported by Manila."),
42 42
     cfg.StrOpt("region",

+ 35
- 0
manila_tempest_tests/services/share/v2/json/shares_client.py View File

@@ -238,6 +238,23 @@ class SharesV2Client(shares_client.SharesClient):
238 238
         self.expected_success(200, resp.status)
239 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 258
     def delete_share(self, share_id, params=None,
242 259
                      version=LATEST_MICROVERSION):
243 260
         uri = "shares/%s" % share_id
@@ -265,6 +282,24 @@ class SharesV2Client(shares_client.SharesClient):
265 282
         self.expected_success(200, resp.status)
266 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 303
     def wait_for_share_instance_status(self, instance_id, status,
269 304
                                        version=LATEST_MICROVERSION):
270 305
         """Waits for a share to reach a given status."""

+ 143
- 0
manila_tempest_tests/tests/api/admin/test_export_locations.py View File

@@ -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
+
16
+from oslo_utils import timeutils
17
+from oslo_utils import uuidutils
18
+import six
19
+from tempest import config
20
+from tempest import test
21
+
22
+from manila_tempest_tests import clients_share as clients
23
+from manila_tempest_tests.tests.api import base
24
+
25
+CONF = config.CONF
26
+
27
+
28
+@base.skip_if_microversion_not_supported("2.9")
29
+class 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
+        )

+ 94
- 0
manila_tempest_tests/tests/api/admin/test_export_locations_negative.py View File

@@ -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
+
16
+from tempest import config
17
+from tempest import test
18
+from tempest_lib import exceptions as lib_exc
19
+
20
+from manila_tempest_tests import clients_share as clients
21
+from manila_tempest_tests.tests.api import base
22
+
23
+CONF = config.CONF
24
+
25
+
26
+@base.skip_if_microversion_not_supported("2.9")
27
+class 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
+                )

+ 25
- 14
manila_tempest_tests/tests/api/admin/test_share_instances.py View File

@@ -17,6 +17,7 @@ from tempest import config
17 17
 from tempest import test
18 18
 
19 19
 from manila_tempest_tests.tests.api import base
20
+from manila_tempest_tests import utils
20 21
 
21 22
 CONF = config.CONF
22 23
 
@@ -58,21 +59,31 @@ class ShareInstancesTest(base.BaseSharesAdminTest):
58 59
         msg = 'Share instance for share %s was not found.' % self.share['id']
59 60
         self.assertIn(self.share['id'], share_ids, msg)
60 61
 
61
-    @test.attr(type=["gate", ])
62
-    def test_get_share_instance_v2_3(self):
62
+    def _get_share_instance(self, version):
63 63
         """Test that we get the proper keys back for the instance."""
64 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'],
68
-                                                      version='2.3')
69
-
70
-        expected_keys = ['host', 'share_id', 'id', 'share_network_id',
71
-                         'status', 'availability_zone', 'share_server_id',
72
-                         'export_locations', 'export_location', 'created_at']
73
-        actual_keys = si.keys()
74
-        self.assertEqual(sorted(expected_keys), sorted(actual_keys),
67
+        si = self.shares_v2_client.get_share_instance(
68
+            share_instances[0]['id'], version=version)
69
+
70
+        expected_keys = [
71
+            'host', 'share_id', 'id', 'share_network_id', 'status',
72
+            'availability_zone', 'share_server_id', 'created_at',
73
+        ]
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 79
                          'Share instance %s returned incorrect keys; '
76
-                         'expected %s, got %s.' % (si['id'],
77
-                                                   sorted(expected_keys),
78
-                                                   sorted(actual_keys)))
80
+                         'expected %s, got %s.' % (
81
+                             si['id'], expected_keys, 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')

+ 2
- 1
manila_tempest_tests/tests/api/base.py View File

@@ -338,7 +338,8 @@ class BaseSharesTest(test.BaseTestCase):
338 338
     def migrate_share(cls, share_id, dest_host, client=None, **kwargs):
339 339
         client = client or cls.shares_v2_client
340 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 343
         return share
343 344
 
344 345
     @classmethod

+ 11
- 1
manila_tempest_tests/tests/api/test_shares.py View File

@@ -19,6 +19,7 @@ from tempest_lib import exceptions as lib_exc  # noqa
19 19
 import testtools  # noqa
20 20
 
21 21
 from manila_tempest_tests.tests.api import base
22
+from manila_tempest_tests import utils
22 23
 
23 24
 CONF = config.CONF
24 25
 
@@ -40,7 +41,7 @@ class SharesNFSTest(base.BaseSharesTest):
40 41
 
41 42
         share = self.create_share(self.protocol)
42 43
         detailed_elements = {'name', 'id', 'availability_zone',
43
-                             'description', 'export_location', 'project_id',
44
+                             'description', 'project_id',
44 45
                              'host', 'created_at', 'share_proto', 'metadata',
45 46
                              'size', 'snapshot_id', 'share_network_id',
46 47
                              'status', 'share_type', 'volume_type', 'links',
@@ -57,6 +58,7 @@ class SharesNFSTest(base.BaseSharesTest):
57 58
 
58 59
         # Get share using v 2.1 - we expect key 'snapshot_support' to be absent
59 60
         share_get = self.shares_v2_client.get_share(share['id'], version='2.1')
61
+        detailed_elements.add('export_location')
60 62
         self.assertTrue(detailed_elements.issubset(share_get.keys()), msg)
61 63
 
62 64
         # Get share using v 2.2 - we expect key 'snapshot_support' to exist
@@ -64,6 +66,14 @@ class SharesNFSTest(base.BaseSharesTest):
64 66
         detailed_elements.add('snapshot_support')
65 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 77
         # Delete share
68 78
         self.shares_v2_client.delete_share(share['id'])
69 79
         self.shares_v2_client.wait_for_resource_deletion(share_id=share['id'])

+ 17
- 5
manila_tempest_tests/tests/api/test_shares_actions.py View File

@@ -82,11 +82,12 @@ class SharesActionsTest(base.BaseSharesTest):
82 82
         # verify keys
83 83
         expected_keys = [
84 84
             "status", "description", "links", "availability_zone",
85
-            "created_at", "export_location", "project_id",
86
-            "export_locations", "volume_type", "share_proto", "name",
85
+            "created_at", "project_id", "volume_type", "share_proto", "name",
87 86
             "snapshot_id", "id", "size", "share_network_id", "metadata",
88 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 91
         if utils.is_microversion_ge(version, '2.2'):
91 92
             expected_keys.append("snapshot_support")
92 93
         if utils.is_microversion_ge(version, '2.4'):
@@ -130,11 +131,16 @@ class SharesActionsTest(base.BaseSharesTest):
130 131
     def test_get_share_with_share_type_name_key(self):
131 132
         self._get_share('2.6')
132 133
 
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
+
133 139
     @test.attr(type=["gate", ])
134 140
     def test_list_shares(self):
135 141
 
136 142
         # list shares
137
-        shares = self.shares_client.list_shares()
143
+        shares = self.shares_v2_client.list_shares()
138 144
 
139 145
         # verify keys
140 146
         keys = ["name", "id", "links"]
@@ -155,11 +161,12 @@ class SharesActionsTest(base.BaseSharesTest):
155 161
         # verify keys
156 162
         keys = [
157 163
             "status", "description", "links", "availability_zone",
158
-            "created_at", "export_location", "project_id",
159
-            "export_locations", "volume_type", "share_proto", "name",
164
+            "created_at", "project_id", "volume_type", "share_proto", "name",
160 165
             "snapshot_id", "id", "size", "share_network_id", "metadata",
161 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 170
         if utils.is_microversion_ge(version, '2.2'):
164 171
             keys.append("snapshot_support")
165 172
         if utils.is_microversion_ge(version, '2.4'):
@@ -194,6 +201,11 @@ class SharesActionsTest(base.BaseSharesTest):
194 201
     def test_list_shares_with_detail_share_type_name_key(self):
195 202
         self._list_shares_with_detail('2.6')
196 203
 
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
+
197 209
     @test.attr(type=["gate", ])
198 210
     def test_list_shares_with_detail_filter_by_metadata(self):
199 211
         filters = {'metadata': self.metadata}

+ 1
- 0
manila_tempest_tests/tests/scenario/manager_share.py View File

@@ -38,6 +38,7 @@ class ShareScenarioTest(manager.NetworkScenarioTest):
38 38
 
39 39
         # Manila clients
40 40
         cls.shares_client = clients_share.Manager().shares_client
41
+        cls.shares_v2_client = clients_share.Manager().shares_v2_client
41 42
         cls.shares_admin_client = clients_share.AdminManager().shares_client
42 43
         cls.shares_admin_v2_client = (
43 44
             clients_share.AdminManager().shares_v2_client)

+ 16
- 4
manila_tempest_tests/tests/scenario/test_share_basic_ops.py View File

@@ -20,6 +