Browse Source

Merge "Implement manage/unmanage support in generic driver"

Jenkins 4 years ago
parent
commit
20e590e8b3

+ 3
- 0
contrib/ci/post_test_hook.sh View File

@@ -40,6 +40,9 @@ iniset $BASE/new/tempest/etc/tempest.conf share share_creation_retry_number 2
40 40
 # Suppress errors in cleanup of resources
41 41
 iniset $BASE/new/tempest/etc/tempest.conf share suppress_errors_in_cleanup True
42 42
 
43
+# Enable manage/unmanage tests
44
+iniset $BASE/new/tempest/etc/tempest.conf share run_manage_unmanage_tests True
45
+
43 46
 # Define whether share drivers handle share servers or not.
44 47
 # Requires defined config option 'driver_handles_share_servers'.
45 48
 MANILA_CONF=${MANILA_CONF:-/etc/manila/manila.conf}

+ 145
- 0
contrib/tempest/tempest/api/share/admin/test_share_manage.py View File

@@ -0,0 +1,145 @@
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_lib import exceptions as lib_exc  # noqa
17
+import testtools  # noqa
18
+
19
+from tempest.api.share import base
20
+from tempest.common.utils import data_utils
21
+from tempest import config_share as config
22
+from tempest import test
23
+
24
+CONF = config.CONF
25
+
26
+
27
+class ManageNFSShareTest(base.BaseSharesAdminTest):
28
+    protocol = 'nfs'
29
+
30
+    # NOTE(vponomaryov): be careful running these tests using generic driver
31
+    # because cinder volumes will stay attached to service Nova VM and
32
+    # won't be deleted.
33
+
34
+    @classmethod
35
+    @testtools.skipIf(
36
+        CONF.share.multitenancy_enabled,
37
+        "Only for driver_handles_share_servers = False driver mode.")
38
+    @testtools.skipUnless(
39
+        CONF.share.run_manage_unmanage_tests,
40
+        "Manage/unmanage tests are disabled.")
41
+    def resource_setup(cls):
42
+        super(ManageNFSShareTest, cls).resource_setup()
43
+        if cls.protocol not in CONF.share.enable_protocols:
44
+            message = "%s tests are disabled" % cls.protocol
45
+            raise cls.skipException(message)
46
+
47
+        # Create share types
48
+        cls.st_name = data_utils.rand_name("manage-st-name")
49
+        cls.st_name_invalid = data_utils.rand_name("manage-st-name-invalid")
50
+        cls.extra_specs = {
51
+            'storage_protocol': CONF.share.storage_protocol,
52
+            'driver_handles_share_servers': False
53
+        }
54
+        cls.extra_specs_invalid = {
55
+            'storage_protocol': CONF.share.storage_protocol,
56
+            'driver_handles_share_servers': True
57
+        }
58
+
59
+        __, cls.st = cls.create_share_type(
60
+            name=cls.st_name,
61
+            cleanup_in_class=True,
62
+            extra_specs=cls.extra_specs)
63
+
64
+        __, cls.st_invalid = cls.create_share_type(
65
+            name=cls.st_name_invalid,
66
+            cleanup_in_class=True,
67
+            extra_specs=cls.extra_specs_invalid)
68
+
69
+        creation_data = {'kwargs': {
70
+            'share_type_id': cls.st['share_type']['id'],
71
+            'share_protocol': cls.protocol,
72
+        }}
73
+        # Create two shares in parallel
74
+        cls.shares = cls.create_shares([creation_data, creation_data])
75
+
76
+        # Load all share data (host, etc.)
77
+        __, cls.share1 = cls.shares_client.get_share(cls.shares[0]['id'])
78
+        __, cls.share2 = cls.shares_client.get_share(cls.shares[1]['id'])
79
+
80
+        # Unmanage shares from manila
81
+        cls.shares_client.unmanage_share(cls.share1['id'])
82
+        cls.shares_client.wait_for_resource_deletion(share_id=cls.share1['id'])
83
+        cls.shares_client.unmanage_share(cls.share2['id'])
84
+        cls.shares_client.wait_for_resource_deletion(share_id=cls.share2['id'])
85
+
86
+    @test.attr(type=["gate", "smoke"])
87
+    def test_manage(self):
88
+        # manage share
89
+        resp, share = self.shares_client.manage_share(
90
+            service_host=self.share1['host'],
91
+            export_path=self.share1['export_locations'][0],
92
+            protocol=self.share1['share_proto'],
93
+            share_type_id=self.st['share_type']['id'])
94
+        self.assertIn(int(resp["status"]), self.HTTP_SUCCESS)
95
+
96
+        # Add managed share to cleanup queue
97
+        self.method_resources.insert(
98
+            0, {'type': 'share_type', 'id': share['id'],
99
+                'client': self.shares_client})
100
+
101
+        # Wait for success
102
+        self.shares_client.wait_for_share_status(share['id'], 'available')
103
+
104
+        # delete share
105
+        resp, __ = self.shares_client.delete_share(share['id'])
106
+        self.assertIn(int(resp["status"]), self.HTTP_SUCCESS)
107
+        self.shares_client.wait_for_resource_deletion(share_id=share['id'])
108
+        self.assertRaises(lib_exc.NotFound,
109
+                          self.shares_client.get_share,
110
+                          share['id'])
111
+
112
+    @test.attr(type=["gate", "smoke"])
113
+    def test_manage_retry(self):
114
+        # manage share with invalid parameters
115
+        share = None
116
+        parameters = [(self.st_invalid['share_type']['id'], 'MANAGE_ERROR'),
117
+                      (self.st['share_type']['id'], 'available')]
118
+
119
+        for share_type_id, status in parameters:
120
+            resp, share = self.shares_client.manage_share(
121
+                service_host=self.share2['host'],
122
+                export_path=self.share2['export_locations'][0],
123
+                protocol=self.share2['share_proto'],
124
+                share_type_id=share_type_id)
125
+            self.assertIn(int(resp["status"]), self.HTTP_SUCCESS)
126
+
127
+            # Add managed share to cleanup queue
128
+            self.method_resources.insert(
129
+                0, {'type': 'share_type', 'id': share['id'],
130
+                    'client': self.shares_client})
131
+
132
+            # Wait for success
133
+            self.shares_client.wait_for_share_status(share['id'], status)
134
+
135
+        # delete share
136
+        resp, __ = self.shares_client.delete_share(share['id'])
137
+        self.assertIn(int(resp["status"]), self.HTTP_SUCCESS)
138
+        self.shares_client.wait_for_resource_deletion(share_id=share['id'])
139
+        self.assertRaises(lib_exc.NotFound,
140
+                          self.shares_client.get_share,
141
+                          share['id'])
142
+
143
+
144
+class ManageCIFSShareTest(ManageNFSShareTest):
145
+    protocol = 'cifs'

+ 5
- 0
contrib/tempest/tempest/config_share.py View File

@@ -108,6 +108,11 @@ ShareGroup = [
108 108
                 help="Whether to suppress errors with clean up operation "
109 109
                      "or not. There are cases when we may want to skip "
110 110
                      "such errors and catch only test errors."),
111
+    cfg.BoolOpt("run_manage_unmanage_tests",
112
+                default=False,
113
+                help="Defines whether to run manage/unmanage tests or not. "
114
+                     "These test may leave orphaned resources, so be careful "
115
+                     "enabling this opt."),
111 116
 ]
112 117
 
113 118
 

+ 18
- 1
contrib/tempest/tempest/services/share/json/shares_client.py View File

@@ -84,6 +84,23 @@ class SharesClient(service_client.ServiceClient):
84 84
     def delete_share(self, share_id):
85 85
         return self.delete("shares/%s" % share_id)
86 86
 
87
+    def manage_share(self, service_host, protocol, export_path,
88
+                     share_type_id):
89
+        post_body = {
90
+            "share": {
91
+                "export_path": export_path,
92
+                "service_host": service_host,
93
+                "protocol": protocol,
94
+                "share_type": share_type_id,
95
+            }
96
+        }
97
+        body = json.dumps(post_body)
98
+        resp, body = self.post("os-share-manage", body)
99
+        return resp, self._parse_resp(body)
100
+
101
+    def unmanage_share(self, share_id):
102
+        return self.post("os-share-unmanage/%s/unmanage" % share_id, None)
103
+
87 104
     def list_shares(self, detailed=False, params=None):
88 105
         """Get list of shares w/o filters."""
89 106
         uri = 'shares/detail' if detailed else 'shares'
@@ -176,7 +193,7 @@ class SharesClient(service_client.ServiceClient):
176 193
             share_status = body['status']
177 194
             if share_status == status:
178 195
                 return
179
-            elif 'error' in share_status:
196
+            elif 'error' in share_status.lower():
180 197
                 raise share_exceptions.\
181 198
                     ShareBuildErrorException(share_id=share_id)
182 199
 

+ 11
- 5
manila/api/contrib/share_manage.py View File

@@ -13,11 +13,11 @@
13 13
 #   under the License.
14 14
 
15 15
 import six
16
-import webob
17 16
 from webob import exc
18 17
 
19 18
 from manila.api import extensions
20 19
 from manila.api.openstack import wsgi
20
+from manila.api.views import shares as share_views
21 21
 from manila import exception
22 22
 from manila.i18n import _
23 23
 from manila import share
@@ -29,6 +29,9 @@ authorize = extensions.extension_authorizer('share', 'manage')
29 29
 
30 30
 
31 31
 class ShareManageController(wsgi.Controller):
32
+
33
+    _view_builder_class = share_views.ViewBuilder
34
+
32 35
     def __init__(self, *args, **kwargs):
33 36
         super(ShareManageController, self).__init__(*args, **kwargs)
34 37
         self.share_api = share.API()
@@ -41,7 +44,7 @@ class ShareManageController(wsgi.Controller):
41 44
         share = {
42 45
             'host': share_data['service_host'],
43 46
             'export_location': share_data['export_path'],
44
-            'share_proto': share_data['protocol'],
47
+            'share_proto': share_data['protocol'].upper(),
45 48
             'share_type_id': share_data['share_type_id'],
46 49
             'display_name': share_data.get('display_name', ''),
47 50
             'display_description': share_data.get('display_description', ''),
@@ -50,13 +53,13 @@ class ShareManageController(wsgi.Controller):
50 53
         driver_options = share_data.get('driver_options', {})
51 54
 
52 55
         try:
53
-            self.share_api.manage(context, share, driver_options)
56
+            share_ref = self.share_api.manage(context, share, driver_options)
54 57
         except exception.PolicyNotAuthorized as e:
55 58
             raise exc.HTTPForbidden(explanation=six.text_type(e))
56 59
         except exception.ManilaException as e:
57 60
             raise exc.HTTPConflict(explanation=six.text_type(e))
58 61
 
59
-        return webob.Response(status_int=202)
62
+        return self._view_builder.detail(req, share_ref)
60 63
 
61 64
     def _validate_manage_parameters(self, context, body):
62 65
         if not (body and self.is_valid_body(body, 'share')):
@@ -71,6 +74,9 @@ class ShareManageController(wsgi.Controller):
71 74
             if parameter not in data:
72 75
                 msg = _("Required parameter %s not found") % parameter
73 76
                 raise exc.HTTPUnprocessableEntity(explanation=msg)
77
+            if not data.get(parameter):
78
+                msg = _("Required parameter %s is empty") % parameter
79
+                raise exc.HTTPUnprocessableEntity(explanation=msg)
74 80
 
75 81
         if not share_utils.extract_host(data['service_host'], 'pool'):
76 82
             msg = _("service_host parameter should contain pool.")
@@ -114,4 +120,4 @@ class Share_manage(extensions.ExtensionDescriptor):
114 120
         controller = ShareManageController()
115 121
         res = extensions.ResourceExtension(Share_manage.alias,
116 122
                                            controller)
117
-        return [res]
123
+        return [res]

+ 4
- 0
manila/exception.py View File

@@ -163,6 +163,10 @@ class InvalidUUID(Invalid):
163 163
     message = _("Expected a uuid but received %(uuid)s.")
164 164
 
165 165
 
166
+class InvalidDriverMode(Invalid):
167
+    message = _("Invalid driver mode: %(driver_mode)s.")
168
+
169
+
166 170
 class NotFound(ManilaException):
167 171
     message = _("Resource could not be found.")
168 172
     code = 404

+ 9
- 3
manila/share/api.py View File

@@ -230,11 +230,13 @@ class API(base.Base):
230 230
             'project_id': context.project_id,
231 231
             'status': constants.STATUS_MANAGING,
232 232
             'scheduled_at': timeutils.utcnow(),
233
-            'deleted': False,
234 233
         })
235 234
 
236
-        retry_states = (constants.STATUS_MANAGE_ERROR,
237
-                        constants.STATUS_UNMANAGED)
235
+        LOG.debug("Manage: Found shares %s" % len(shares))
236
+
237
+        retry_states = (constants.STATUS_MANAGE_ERROR,)
238
+
239
+        export_location = share_data.pop('export_location')
238 240
 
239 241
         if len(shares) == 0:
240 242
             share = self.db.share_create(context, share_data)
@@ -246,7 +248,11 @@ class API(base.Base):
246 248
             msg = _("Share already exists.")
247 249
             raise exception.ManilaException(msg)
248 250
 
251
+        self.db.share_export_locations_update(context, share['id'],
252
+                                              export_location)
253
+
249 254
         self.share_rpcapi.manage_share(context, share, driver_options)
255
+        return self.db.share_get(context, share['id'])
250 256
 
251 257
     def unmanage(self, context, share):
252 258
         policy.check_policy(context, 'share', 'unmanage')

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

@@ -24,6 +24,7 @@ from oslo_config import cfg
24 24
 from oslo_log import log
25 25
 from oslo_utils import excutils
26 26
 from oslo_utils import importutils
27
+from oslo_utils import strutils
27 28
 import six
28 29
 
29 30
 from manila.common import constants as const
@@ -35,6 +36,7 @@ from manila.i18n import _LE
35 36
 from manila.i18n import _LW
36 37
 from manila.share import driver
37 38
 from manila.share.drivers import service_instance
39
+from manila.share import share_types
38 40
 from manila import utils
39 41
 from manila import volume
40 42
 
@@ -206,9 +208,8 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
206 208
                    volume['mountpoint']]
207 209
         self._ssh_exec(server_details, command)
208 210
 
209
-    def _is_device_mounted(self, share, server_details, volume=None):
211
+    def _is_device_mounted(self, mount_path, server_details, volume=None):
210 212
         """Checks whether volume already mounted or not."""
211
-        mount_path = self._get_mount_path(share)
212 213
         log_data = {
213 214
             'mount_path': mount_path,
214 215
             'server_id': server_details['instance_id'],
@@ -274,7 +275,8 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
274 275
                 'server': server_details['instance_id'],
275 276
             }
276 277
             try:
277
-                if not self._is_device_mounted(share, server_details, volume):
278
+                if not self._is_device_mounted(mount_path, server_details,
279
+                                               volume):
278 280
                     LOG.debug("Mounting '%(dev)s' to path '%(path)s' on "
279 281
                               "server '%(server)s'.", log_data)
280 282
                     mount_cmd = ['sudo mkdir -p', mount_path, '&&']
@@ -303,7 +305,7 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
303 305
                 'path': mount_path,
304 306
                 'server': server_details['instance_id'],
305 307
             }
306
-            if self._is_device_mounted(share, server_details):
308
+            if self._is_device_mounted(mount_path, server_details):
307 309
                 LOG.debug("Unmounting path '%(path)s' on server "
308 310
                           "'%(server)s'.", log_data)
309 311
                 unmount_cmd = ['sudo umount', mount_path, '&& sudo rmdir',
@@ -354,24 +356,28 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
354 356
                     self.configuration.max_time_to_attach)
355 357
         return do_attach(volume)
356 358
 
359
+    def _get_volume_name(self, share_id):
360
+        return self.configuration.volume_name_template % share_id
361
+
357 362
     def _get_volume(self, context, share_id):
358 363
         """Finds volume, associated to the specific share."""
359
-        volume_name = self.configuration.volume_name_template % share_id
364
+        volume_name = self._get_volume_name(share_id)
360 365
         search_opts = {'name': volume_name}
361 366
         if context.is_admin:
362 367
             search_opts['all_tenants'] = True
363 368
         volumes_list = self.volume_api.get_all(context, search_opts)
364
-        volume = None
365 369
         if len(volumes_list) == 1:
366
-            volume = volumes_list[0]
370
+            return volumes_list[0]
367 371
         elif len(volumes_list) > 1:
368 372
             LOG.error(
369 373
                 _LE("Expected only one volume in volume list with name "
370 374
                     "'%(name)s', but got more than one in a result - "
371 375
                     "'%(result)s'."), {
372 376
                         'name': volume_name, 'result': volumes_list})
373
-            raise exception.ManilaException(_('Error. Ambiguous volumes'))
374
-        return volume
377
+            raise exception.ManilaException(
378
+                _("Error. Ambiguous volumes for name '%s'") % volume_name)
379
+        else:
380
+            raise exception.VolumeNotFound(volume_id=volume_name)
375 381
 
376 382
     def _get_volume_snapshot(self, context, snapshot_id):
377 383
         """Find volume snapshot associated to the specific share snapshot."""
@@ -633,6 +639,107 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
633 639
         self.service_instance_manager.delete_service_instance(
634 640
             self.admin_context, server_details)
635 641
 
642
+    def manage_existing(self, share, driver_options):
643
+        """Manage existing share to manila.
644
+
645
+        Generic driver accepts only one driver_option 'volume_id'.
646
+        If an administrator provides this option, then appropriate Cinder
647
+        volume will be managed by Manila as well.
648
+
649
+        :param share: share data
650
+        :param driver_options: Empty dict or dict with 'volume_id' option.
651
+        :return: dict with share size, example: {'size': 1}
652
+        """
653
+        if self.driver_handles_share_servers:
654
+            msg = _('Operation "manage" for shares is supported only when '
655
+                    'driver does not handle share servers.')
656
+            raise exception.InvalidDriverMode(msg)
657
+
658
+        helper = self._get_helper(share)
659
+        driver_mode = share_types.get_share_type_extra_specs(
660
+            share['share_type_id'],
661
+            const.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS)
662
+
663
+        if strutils.bool_from_string(driver_mode):
664
+            msg = _("%(mode)s != False") % {
665
+                'mode': const.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS
666
+            }
667
+            raise exception.ManageExistingShareTypeMismatch(reason=msg)
668
+
669
+        share_server = self.service_instance_manager.get_common_server()
670
+        server_details = share_server['backend_details']
671
+
672
+        mount_path = helper.get_share_path_by_export_location(
673
+            share_server['backend_details'], share['export_locations'][0])
674
+        LOG.debug("Manage: mount path = %s", mount_path)
675
+
676
+        mounted = self._is_device_mounted(mount_path, server_details)
677
+        LOG.debug("Manage: is share mounted = %s", mounted)
678
+
679
+        if not mounted:
680
+            msg = _("Provided share %s is not mounted.") % share['id']
681
+            raise exception.ManageInvalidShare(reason=msg)
682
+
683
+        def get_volume():
684
+            if 'volume_id' in driver_options:
685
+                try:
686
+                    return self.volume_api.get(
687
+                        self.admin_context, driver_options['volume_id'])
688
+                except exception.VolumeNotFound as e:
689
+                    raise exception.ManageInvalidShare(reason=six.text_type(e))
690
+
691
+            # NOTE(vponomaryov): Manila can only combine volume name by itself,
692
+            # nowhere to get volume ID from. Return None since Cinder volume
693
+            # names are not unique or fixed, hence, they can not be used for
694
+            # sure.
695
+            return None
696
+
697
+        share_volume = get_volume()
698
+
699
+        if share_volume:
700
+            instance_volumes = self.compute_api.instance_volumes_list(
701
+                self.admin_context, server_details['instance_id'])
702
+
703
+            attached_volumes = [vol.id for vol in instance_volumes]
704
+            LOG.debug('Manage: attached volumes = %s',
705
+                      six.text_type(attached_volumes))
706
+
707
+            if share_volume['id'] not in attached_volumes:
708
+                msg = _("Provided volume %s is not attached "
709
+                        "to service instance.") % share_volume['id']
710
+                raise exception.ManageInvalidShare(reason=msg)
711
+
712
+            linked_volume_name = self._get_volume_name(share['id'])
713
+            if share_volume['name'] != linked_volume_name:
714
+                LOG.debug('Manage: volume_id = %s' % share_volume['id'])
715
+                self.volume_api.update(self.admin_context, share_volume['id'],
716
+                                       {'name': linked_volume_name})
717
+
718
+            share_size = share_volume['size']
719
+        else:
720
+            share_size = self._get_mounted_share_size(
721
+                mount_path, share_server['backend_details'])
722
+
723
+        # TODO(vponomaryov): need update export_locations too using driver's
724
+        # information.
725
+        return {'size': share_size}
726
+
727
+    def _get_mounted_share_size(self, mount_path, server_details):
728
+        share_size_cmd = ['df', '-PBG', mount_path]
729
+        output, __ = self._ssh_exec(server_details, share_size_cmd)
730
+        lines = output.split('\n')
731
+
732
+        try:
733
+            size = int(lines[1].split()[1][:-1])
734
+        except Exception as e:
735
+            msg = _("Cannot calculate size of share %(path)s : %(error)s") % {
736
+                'path': mount_path,
737
+                'error': six.text_type(e)
738
+            }
739
+            raise exception.ManageInvalidShare(reason=msg)
740
+
741
+        return size
742
+
636 743
 
637 744
 class NASHelperBase(object):
638 745
     """Interface to work with share."""
@@ -662,6 +769,10 @@ class NASHelperBase(object):
662 769
         """Deny access to the host."""
663 770
         raise NotImplementedError()
664 771
 
772
+    def get_share_path_by_export_location(self, server, export_location):
773
+        """Returns share path by its export location."""
774
+        raise NotImplementedError()
775
+
665 776
 
666 777
 def nfs_synchronized(f):
667 778
 
@@ -745,6 +856,9 @@ class NFSHelper(NASHelperBase):
745 856
         ]
746 857
         self._ssh_exec(server, sync_cmd)
747 858
 
859
+    def get_share_path_by_export_location(self, server, export_location):
860
+        return export_location.split(':')[-1]
861
+
748 862
 
749 863
 class CIFSHelper(NASHelperBase):
750 864
     """Manage shares in samba server by net conf tool.
@@ -855,3 +969,21 @@ class CIFSHelper(NASHelperBase):
855 969
         value = "\"" + ' '.join(hosts) + "\""
856 970
         self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name,
857 971
                                 '\"hosts allow\"', value])
972
+
973
+    def get_share_path_by_export_location(self, server, export_location):
974
+        # Get name of group that contains share data on CIFS server
975
+        if export_location.startswith('\\\\'):
976
+            group_name = export_location.split('\\')[-1]
977
+        elif export_location.startswith('//'):
978
+            group_name = export_location.split('/')[-1]
979
+        else:
980
+            msg = _("Got incorrect CIFS export location "
981
+                    "'%s'.") % export_location
982
+            raise exception.InvalidShare(reason=msg)
983
+
984
+        # Get parameter 'path' from group that belongs to current share
985
+        (out, __) = self._ssh_exec(
986
+            server, ['sudo', 'net', 'conf', 'getparm', group_name, 'path'])
987
+
988
+        # Remove special symbols from response and return path
989
+        return out.strip()

+ 9
- 7
manila/share/manager.py View File

@@ -340,8 +340,7 @@ class ShareManager(manager.SchedulerDependentManager):
340 340
                 raise exception.InvalidShare(reason=msg)
341 341
 
342 342
             share_update = (
343
-                self.driver.manage_existing(share_ref, driver_options) or {}
344
-            )
343
+                self.driver.manage_existing(share_ref, driver_options) or {})
345 344
 
346 345
             if not share_update.get('size'):
347 346
                 msg = _("Driver cannot calculate share size.")
@@ -349,19 +348,22 @@ class ShareManager(manager.SchedulerDependentManager):
349 348
 
350 349
             self._update_quota_usages(context, project_id, {
351 350
                 "shares": 1,
352
-                "gigabytes": share_update['size']
351
+                "gigabytes": share_update['size'],
353 352
             })
354 353
 
355 354
             share_update.update({
356 355
                 'status': 'available',
357
-                'launched_at': timeutils.utcnow()
356
+                'launched_at': timeutils.utcnow(),
358 357
             })
359 358
             self.db.share_update(context, share_id, share_update)
360
-
361 359
         except Exception as e:
362 360
             LOG.error(_LW("Manage share failed: %s"), six.text_type(e))
363
-            self.db.share_update(context, share_id,
364
-                                 {'status': constants.STATUS_MANAGE_ERROR})
361
+            # NOTE(vponomaryov): set size as 1 because design expects size
362
+            # to be set, it also will allow us to handle delete/unmanage
363
+            # operations properly with this errored share according to quotas.
364
+            self.db.share_update(
365
+                context, share_id,
366
+                {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
365 367
 
366 368
     def _update_quota_usages(self, context, project_id, usages):
367 369
         user_id = context.user_id

+ 5
- 3
manila/tests/api/contrib/test_share_manage.py View File

@@ -141,12 +141,14 @@ class ShareManageTest(test.TestCase):
141 141
     def test_share_manage(self):
142 142
         body = get_fake_manage_body()
143 143
         self._setup_manage_mocks()
144
-        self.mock_object(share_api.API, 'manage')
144
+        share = {'share_type_id': '', 'id': 'fake'}
145
+        self.mock_object(share_api.API, 'manage',
146
+                         mock.Mock(return_value=share))
145 147
 
146 148
         share = {
147 149
             'host': body['share']['service_host'],
148 150
             'export_location': body['share']['export_path'],
149
-            'share_proto': body['share']['protocol'],
151
+            'share_proto': body['share']['protocol'].upper(),
150 152
             'share_type_id': 'fake',
151 153
             'display_name': body['share']['display_name'],
152 154
             'display_description': body['share']['display_description'],
@@ -156,7 +158,7 @@ class ShareManageTest(test.TestCase):
156 158
 
157 159
         share_api.API.manage.assert_called_once_with(
158 160
             mock.ANY, share, {})
159
-        self.assertEqual(202, actual_result.status_int)
161
+        self.assertIsNotNone(actual_result)
160 162
 
161 163
     def test_wrong_permissions(self):
162 164
         body = get_fake_manage_body()

+ 205
- 28
manila/tests/share/drivers/test_generic.py View File

@@ -29,6 +29,7 @@ from manila import context
29 29
 from manila import exception
30 30
 import manila.share.configuration
31 31
 from manila.share.drivers import generic
32
+from manila.share import share_types
32 33
 from manila import test
33 34
 from manila.tests import fake_compute
34 35
 from manila.tests import fake_service_instance
@@ -41,6 +42,16 @@ from manila import volume
41 42
 CONF = cfg.CONF
42 43
 
43 44
 
45
+def get_fake_manage_share():
46
+    return {
47
+        'id': 'fake',
48
+        'share_proto': 'NFS',
49
+        'share_type_id': 'fake',
50
+        'export_locations': [
51
+            '10.0.0.1:/foo/fake/path', '11.0.0.1:/bar/fake/path'],
52
+    }
53
+
54
+
44 55
 @ddt.ddt
45 56
 class GenericShareDriverTestCase(test.TestCase):
46 57
     """Tests GenericShareDriver."""
@@ -185,21 +196,18 @@ class GenericShareDriverTestCase(test.TestCase):
185 196
 
186 197
     def test_mount_device_not_present(self):
187 198
         server = {'instance_id': 'fake_server_id'}
188
-        mount_path = '/fake/mount/path'
199
+        mount_path = self._driver._get_mount_path(self.share)
189 200
         volume = {'mountpoint': 'fake_mount_point'}
190 201
         self.mock_object(self._driver, '_is_device_mounted',
191 202
                          mock.Mock(return_value=False))
192 203
         self.mock_object(self._driver, '_sync_mount_temp_and_perm_files')
193
-        self.mock_object(self._driver, '_get_mount_path',
194
-                         mock.Mock(return_value=mount_path))
195 204
         self.mock_object(self._driver, '_ssh_exec',
196 205
                          mock.Mock(return_value=('', '')))
197 206
 
198 207
         self._driver._mount_device(self.share, server, volume)
199 208
 
200
-        self._driver._get_mount_path.assert_called_once_with(self.share)
201 209
         self._driver._is_device_mounted.assert_called_once_with(
202
-            self.share, server, volume)
210
+            mount_path, server, volume)
203 211
         self._driver._sync_mount_temp_and_perm_files.assert_called_once_with(
204 212
             server)
205 213
         self._driver._ssh_exec.assert_called_once_with(
@@ -222,13 +230,12 @@ class GenericShareDriverTestCase(test.TestCase):
222 230
 
223 231
         self._driver._get_mount_path.assert_called_once_with(self.share)
224 232
         self._driver._is_device_mounted.assert_called_once_with(
225
-            self.share, self.server, volume)
233
+            mount_path, self.server, volume)
226 234
         generic.LOG.warning.assert_called_once_with(mock.ANY, mock.ANY)
227 235
 
228 236
     def test_mount_device_exception_raised(self):
229 237
         volume = {'mountpoint': 'fake_mount_point'}
230
-        self.mock_object(self._driver, '_get_mount_path',
231
-                         mock.Mock(return_value='fake'))
238
+
232 239
         self.mock_object(
233 240
             self._driver, '_is_device_mounted',
234 241
             mock.Mock(side_effect=exception.ProcessExecutionError))
@@ -240,9 +247,8 @@ class GenericShareDriverTestCase(test.TestCase):
240 247
             self.server,
241 248
             volume,
242 249
         )
243
-        self._driver._get_mount_path.assert_called_once_with(self.share)
244 250
         self._driver._is_device_mounted.assert_called_once_with(
245
-            self.share, self.server, volume)
251
+            self._driver._get_mount_path(self.share), self.server, volume)
246 252
 
247 253
     def test_unmount_device_present(self):
248 254
         mount_path = '/fake/mount/path'
@@ -258,7 +264,7 @@ class GenericShareDriverTestCase(test.TestCase):
258 264
 
259 265
         self._driver._get_mount_path.assert_called_once_with(self.share)
260 266
         self._driver._is_device_mounted.assert_called_once_with(
261
-            self.share, self.server)
267
+            mount_path, self.server)
262 268
         self._driver._sync_mount_temp_and_perm_files.assert_called_once_with(
263 269
             self.server)
264 270
         self._driver._ssh_exec.assert_called_once_with(
@@ -278,7 +284,7 @@ class GenericShareDriverTestCase(test.TestCase):
278 284
 
279 285
         self._driver._get_mount_path.assert_called_once_with(self.share)
280 286
         self._driver._is_device_mounted.assert_called_once_with(
281
-            self.share, self.server)
287
+            mount_path, self.server)
282 288
         generic.LOG.warning.assert_called_once_with(mock.ANY, mock.ANY)
283 289
 
284 290
     def test_is_device_mounted_true(self):
@@ -288,13 +294,10 @@ class GenericShareDriverTestCase(test.TestCase):
288 294
                                           'path': mount_path}
289 295
         self.mock_object(self._driver, '_ssh_exec',
290 296
                          mock.Mock(return_value=(mounts, '')))
291
-        self.mock_object(self._driver, '_get_mount_path',
292
-                         mock.Mock(return_value=mount_path))
293 297
 
294 298
         result = self._driver._is_device_mounted(
295
-            self.share, self.server, volume)
299
+            mount_path, self.server, volume)
296 300
 
297
-        self._driver._get_mount_path.assert_called_once_with(self.share)
298 301
         self._driver._ssh_exec.assert_called_once_with(
299 302
             self.server, ['sudo', 'mount'])
300 303
         self.assertEqual(result, True)
@@ -304,12 +307,9 @@ class GenericShareDriverTestCase(test.TestCase):
304 307
         mounts = "/fake/dev/path on %(path)s type fake" % {'path': mount_path}
305 308
         self.mock_object(self._driver, '_ssh_exec',
306 309
                          mock.Mock(return_value=(mounts, '')))
307
-        self.mock_object(self._driver, '_get_mount_path',
308
-                         mock.Mock(return_value=mount_path))
309 310
 
310
-        result = self._driver._is_device_mounted(self.share, self.server)
311
+        result = self._driver._is_device_mounted(mount_path, self.server)
311 312
 
312
-        self._driver._get_mount_path.assert_called_once_with(self.share)
313 313
         self._driver._ssh_exec.assert_called_once_with(
314 314
             self.server, ['sudo', 'mount'])
315 315
         self.assertEqual(result, True)
@@ -321,13 +321,10 @@ class GenericShareDriverTestCase(test.TestCase):
321 321
                                           'path': mount_path}
322 322
         self.mock_object(self._driver, '_ssh_exec',
323 323
                          mock.Mock(return_value=(mounts, '')))
324
-        self.mock_object(self._driver, '_get_mount_path',
325
-                         mock.Mock(return_value=mount_path))
326 324
 
327 325
         result = self._driver._is_device_mounted(
328
-            self.share, self.server, volume)
326
+            mount_path, self.server, volume)
329 327
 
330
-        self._driver._get_mount_path.assert_called_once_with(self.share)
331 328
         self._driver._ssh_exec.assert_called_once_with(
332 329
             self.server, ['sudo', 'mount'])
333 330
         self.assertEqual(result, False)
@@ -340,9 +337,8 @@ class GenericShareDriverTestCase(test.TestCase):
340 337
         self.mock_object(self._driver, '_get_mount_path',
341 338
                          mock.Mock(return_value=mount_path))
342 339
 
343
-        result = self._driver._is_device_mounted(self.share, self.server)
340
+        result = self._driver._is_device_mounted(mount_path, self.server)
344 341
 
345
-        self._driver._get_mount_path.assert_called_once_with(self.share)
346 342
         self._driver._ssh_exec.assert_called_once_with(
347 343
             self.server, ['sudo', 'mount'])
348 344
         self.assertEqual(result, False)
@@ -467,8 +463,12 @@ class GenericShareDriverTestCase(test.TestCase):
467 463
             self._driver.configuration.volume_name_template % self.share['id'])
468 464
         self.mock_object(self._driver.volume_api, 'get_all',
469 465
                          mock.Mock(return_value=[]))
470
-        result = self._driver._get_volume(self._context, self.share['id'])
471
-        self.assertEqual(result, None)
466
+
467
+        self.assertRaises(
468
+            exception.VolumeNotFound,
469
+            self._driver._get_volume,
470
+            self._context, self.share['id'])
471
+
472 472
         self._driver.volume_api.get_all.assert_called_once_with(
473 473
             self._context, {'all_tenants': True, 'name': vol_name})
474 474
 
@@ -1011,6 +1011,183 @@ class GenericShareDriverTestCase(test.TestCase):
1011 1011
         self.assertEqual(True, result['driver_handles_share_servers'])
1012 1012
         self.assertEqual('Open Source', result['vendor_name'])
1013 1013
 
1014
+    def test_manage_invalid_driver_mode(self):
1015
+        CONF.set_default('driver_handles_share_servers', True)
1016
+
1017
+        self.assertRaises(exception.InvalidDriverMode,
1018
+                          self._driver.manage_existing, 'fake', {})
1019
+
1020
+    def _setup_manage_mocks(self,
1021
+                            get_share_type_extra_specs='False',
1022
+                            is_device_mounted=True,
1023
+                            server_details=None):
1024
+        CONF.set_default('driver_handles_share_servers', False)
1025
+
1026
+        self.mock_object(share_types, 'get_share_type_extra_specs',
1027
+                         mock.Mock(return_value=get_share_type_extra_specs))
1028
+
1029
+        self.mock_object(self._driver, '_is_device_mounted',
1030
+                         mock.Mock(return_value=is_device_mounted))
1031
+
1032
+        self.mock_object(self._driver, 'service_instance_manager')
1033
+        server = {'backend_details': server_details}
1034
+        self.mock_object(self._driver.service_instance_manager,
1035
+                         'get_common_server',
1036
+                         mock.Mock(return_value=server))
1037
+
1038
+    def test_manage_invalid_protocol(self):
1039
+        share = {'share_proto': 'fake_proto'}
1040
+        self._setup_manage_mocks()
1041
+
1042
+        self.assertRaises(exception.InvalidShare,
1043
+                          self._driver.manage_existing, share, {})
1044
+
1045
+    def test_manage_share_type_mismatch(self):
1046
+        share = {'share_proto': 'NFS', 'share_type_id': 'fake'}
1047
+        self._setup_manage_mocks(get_share_type_extra_specs='True')
1048
+
1049
+        self.assertRaises(exception.ManageExistingShareTypeMismatch,
1050
+                          self._driver.manage_existing, share, {})
1051
+
1052
+        share_types.get_share_type_extra_specs.assert_called_once_with(
1053
+            share['share_type_id'],
1054
+            const.ExtraSpecs.DRIVER_HANDLES_SHARE_SERVERS
1055
+        )
1056
+
1057
+    def test_manage_not_mounted_share(self):
1058
+        share = get_fake_manage_share()
1059
+        fake_path = '/foo/bar'
1060
+        self._setup_manage_mocks(is_device_mounted=False)
1061
+        self.mock_object(
1062
+            self._driver._helpers[share['share_proto']],
1063
+            'get_share_path_by_export_location',
1064
+            mock.Mock(return_value=fake_path))
1065
+
1066
+        self.assertRaises(exception.ManageInvalidShare,
1067
+                          self._driver.manage_existing, share, {})
1068
+
1069
+        self.assertEqual(
1070
+            1,
1071
+            self._driver.service_instance_manager.get_common_server.call_count)
1072
+        self._driver._is_device_mounted.assert_called_once_with(
1073
+            fake_path, None)
1074
+        self._driver._helpers[share['share_proto']].\
1075
+            get_share_path_by_export_location.assert_called_once_with(
1076
+                None, share['export_locations'][0])
1077
+
1078
+    def test_manage_share_not_attached_to_cinder_volume_invalid_size(self):
1079
+        share = get_fake_manage_share()
1080
+        server_details = {}
1081
+        fake_path = '/foo/bar'
1082
+        self._setup_manage_mocks(server_details=server_details)
1083
+        self.mock_object(self._driver, '_get_volume',
1084
+                         mock.Mock(return_value=None))
1085
+        error = exception.ManageInvalidShare(reason="fake")
1086
+        self.mock_object(
1087
+            self._driver, '_get_mounted_share_size',
1088
+            mock.Mock(side_effect=error))
1089
+        self.mock_object(
1090
+            self._driver._helpers[share['share_proto']],
1091
+            'get_share_path_by_export_location',
1092
+            mock.Mock(return_value=fake_path))
1093
+
1094
+        self.assertRaises(exception.ManageInvalidShare,
1095
+                          self._driver.manage_existing, share, {})
1096
+
1097
+        self._driver._get_mounted_share_size.assert_called_once_with(
1098
+            fake_path, server_details)
1099
+        self._driver._helpers[share['share_proto']].\
1100
+            get_share_path_by_export_location.assert_called_once_with(
1101
+                server_details, share['export_locations'][0])
1102
+
1103
+    def test_manage_share_not_attached_to_cinder_volume(self):
1104
+        share = get_fake_manage_share()
1105
+        share_size = "fake"
1106
+        server_details = {}
1107
+        self._setup_manage_mocks(server_details=server_details)
1108
+        self.mock_object(self._driver, '_get_volume',
1109
+                         mock.Mock(return_value=None))
1110
+        self.mock_object(self._driver, '_get_mounted_share_size',
1111
+                         mock.Mock(return_value=share_size))
1112
+
1113
+        actual_result = self._driver.manage_existing(share, {})
1114
+        self.assertEqual({'size': share_size}, actual_result)
1115
+
1116
+    def test_manage_share_attached_to_cinder_volume_not_found(self):
1117
+        share = get_fake_manage_share()
1118
+        server_details = {}
1119
+        driver_options = {'volume_id': 'fake'}
1120
+        self._setup_manage_mocks(server_details=server_details)
1121
+        self.mock_object(
1122
+            self._driver.volume_api, 'get',
1123
+            mock.Mock(side_effect=exception.VolumeNotFound(volume_id="fake"))
1124
+        )
1125
+
1126
+        self.assertRaises(exception.ManageInvalidShare,
1127
+                          self._driver.manage_existing, share, driver_options)
1128
+
1129
+        self._driver.volume_api.get.assert_called_once_with(
1130
+            mock.ANY, driver_options['volume_id'])
1131
+
1132
+    def test_manage_share_attached_to_cinder_volume_not_mounted_to_srv(self):
1133
+        share = get_fake_manage_share()
1134
+        server_details = {'instance_id': 'fake'}
1135
+        driver_options = {'volume_id': 'fake'}
1136
+        volume = {'id': 'fake'}
1137
+        self._setup_manage_mocks(server_details=server_details)
1138
+        self.mock_object(self._driver.volume_api, 'get',
1139
+                         mock.Mock(return_value=volume))
1140
+        self.mock_object(self._driver.compute_api, 'instance_volumes_list',
1141
+                         mock.Mock(return_value=[]))
1142
+
1143
+        self.assertRaises(exception.ManageInvalidShare,
1144
+                          self._driver.manage_existing, share, driver_options)
1145
+
1146
+        self._driver.volume_api.get.assert_called_once_with(
1147
+            mock.ANY, driver_options['volume_id'])
1148
+        self._driver.compute_api.instance_volumes_list.assert_called_once_with(
1149
+            mock.ANY, server_details['instance_id'])
1150
+
1151
+    def test_manage_share_attached_to_cinder_volume(self):
1152
+        share = get_fake_manage_share()
1153
+        server_details = {'instance_id': 'fake'}
1154
+        driver_options = {'volume_id': 'fake'}
1155
+        volume = {'id': 'fake', 'name': 'fake_volume_1', 'size': 'fake'}
1156
+        self._setup_manage_mocks(server_details=server_details)
1157
+        self.mock_object(self._driver.volume_api, 'get',
1158
+                         mock.Mock(return_value=volume))
1159
+        self._driver.volume_api.update = mock.Mock()
1160
+        fake_volume = mock.Mock()
1161
+        fake_volume.id = 'fake'
1162
+        self.mock_object(self._driver.compute_api, 'instance_volumes_list',
1163
+                         mock.Mock(return_value=[fake_volume]))
1164
+
1165
+        actual_result = self._driver.manage_existing(share, driver_options)
1166
+
1167
+        self.assertEqual({'size': 'fake'}, actual_result)
1168
+        expected_volume_update = {
1169
+            'name': self._driver._get_volume_name(share['id'])
1170
+        }
1171
+        self._driver.volume_api.update.assert_called_once_with(
1172
+            mock.ANY, volume['id'], expected_volume_update)
1173
+
1174
+    def test_get_mounted_share_size(self):
1175
+        output = ("Filesystem   blocks  Used Available Capacity Mounted on\n"
1176
+                  "/dev/fake  1G  1G  1G  4% /shares/share-fake")
1177
+        self.mock_object(self._driver, '_ssh_exec',
1178
+                         mock.Mock(return_value=(output, '')))
1179
+
1180
+        actual_result = self._driver._get_mounted_share_size('/fake/path', {})
1181
+        self.assertEqual(1, actual_result)
1182
+
1183
+    @ddt.data("fake\nfake\n", "fake", "fake\n")
1184
+    def test_get_mounted_share_size_invalid_output(self, output):
1185
+        self.mock_object(self._driver, '_ssh_exec',
1186
+                         mock.Mock(return_value=(output, '')))
1187
+        self.assertRaises(exception.ManageInvalidShare,
1188
+                          self._driver._get_mounted_share_size,
1189
+                          '/fake/path', {})
1190
+
1014 1191
 
1015 1192
 @generic.ensure_server
1016 1193
 def fake(driver_instance, context, share_server=None):

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

@@ -550,6 +550,9 @@ class ShareAPITestCase(test.TestCase):
550 550
                            status='creating')
551 551
         self.mock_object(db_driver, 'share_create',
552 552
                          mock.Mock(return_value=share))
553
+        self.mock_object(db_driver, 'share_export_locations_update')
554
+        self.mock_object(db_driver, 'share_get',
555
+                         mock.Mock(return_value=share))
553 556
         self.mock_object(self.api, 'get_all', mock.Mock(return_value=[]))
554 557
 
555 558
         self.api.manage(self.context,
@@ -561,17 +564,20 @@ class ShareAPITestCase(test.TestCase):
561 564
             'project_id': self.context.project_id,
562 565
             'status': constants.STATUS_MANAGING,
563 566
             'scheduled_at': date,
564
-            'deleted': False,
565 567
         })
566 568
 
569
+        export_location = share_data.pop('export_location')
567 570
         self.api.get_all.assert_called_once_with(self.context, mock.ANY)
568 571
         db_driver.share_create.assert_called_once_with(self.context,
569 572
                                                        share_data)
573
+        db_driver.share_get.assert_called_once_with(self.context, share['id'])
574
+        db_driver.share_export_locations_update.assert_called_once_with(
575
+            self.context, share['id'], export_location
576
+        )
570 577
         self.share_rpcapi.manage_share.assert_called_once_with(
571 578
             self.context, share, driver_options)
572 579
 
573
-    @ddt.data([{'id': 'fake', 'status': constants.STATUS_MANAGE_ERROR}],
574
-              [{'id': 'fake', 'status': constants.STATUS_UNMANAGED}],)
580
+    @ddt.data([{'id': 'fake', 'status': constants.STATUS_MANAGE_ERROR}])
575 581
     def test_manage_retry(self, shares):
576 582
         share_data = {
577 583
             'host': 'fake',
@@ -579,8 +585,12 @@ class ShareAPITestCase(test.TestCase):
579 585
             'share_proto': 'fake',
580 586
         }
581 587
         driver_options = {}
588
+        share = fake_share('fakeid')
582 589
         self.mock_object(db_driver, 'share_update',
583
-                         mock.Mock(return_value=fake_share('fakeid')))
590
+                         mock.Mock(return_value=share))
591
+        self.mock_object(db_driver, 'share_get',
592
+                         mock.Mock(return_value=share))
593
+        self.mock_object(db_driver, 'share_export_locations_update')
584 594
         self.mock_object(self.api, 'get_all',
585 595
                          mock.Mock(return_value=shares))
586 596
 
@@ -592,6 +602,9 @@ class ShareAPITestCase(test.TestCase):
592 602
             self.context, 'fake', mock.ANY)
593 603
         self.share_rpcapi.manage_share.assert_called_once_with(
594 604
             self.context, mock.ANY, driver_options)
605
+        db_driver.share_export_locations_update.assert_called_once_with(
606
+            self.context, share['id'], mock.ANY
607
+        )
595 608
 
596 609
     def test_manage_duplicate(self):
597 610
         share_data = {

+ 8
- 8
manila/tests/share/test_manager.py View File

@@ -710,8 +710,8 @@ class ShareManagerTestCase(test.TestCase):
710 710
         self.share_manager.manage_share(self.context, share_id, {})
711 711
 
712 712
         self.share_manager.db.share_update.assert_called_once_with(
713
-            mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR}
714
-        )
713
+            mock.ANY, share_id,
714
+            {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
715 715
 
716 716
     def test_manage_share_driver_exception(self):
717 717
         self.mock_object(self.share_manager, 'driver', mock.Mock())
@@ -729,8 +729,8 @@ class ShareManagerTestCase(test.TestCase):
729 729
             assert_called_once_with(mock.ANY, driver_options)
730 730
 
731 731
         self.share_manager.db.share_update.assert_called_once_with(
732
-            mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR}
733
-        )
732
+            mock.ANY, share_id,
733
+            {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
734 734
 
735 735
     def test_manage_share_invalid_size(self):
736 736
         self.mock_object(self.share_manager, 'driver')
@@ -749,8 +749,8 @@ class ShareManagerTestCase(test.TestCase):
749 749
             assert_called_once_with(mock.ANY, driver_options)
750 750
 
751 751
         self.share_manager.db.share_update.assert_called_once_with(
752
-            mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR}
753
-        )
752
+            mock.ANY, share_id,
753
+            {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
754 754
 
755 755
     def test_manage_share_quota_error(self):
756 756
         self.mock_object(self.share_manager, 'driver')
@@ -771,8 +771,8 @@ class ShareManagerTestCase(test.TestCase):
771 771
             assert_called_once_with(mock.ANY, driver_options)
772 772
 
773 773
         self.share_manager.db.share_update.assert_called_once_with(
774
-            mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR}
775
-        )
774
+            mock.ANY, share_id,
775
+            {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
776 776
 
777 777
     @ddt.data({'size': 1},
778 778
               {'size': 1, 'name': 'fake'})

+ 12
- 2
manila/tests/volume/test_cinder.py View File

@@ -129,8 +129,18 @@ class CinderApiTestCase(test.TestCase):
129 129
         self.assertIsNone(self.api.check_detach(self.ctx, volume))
130 130
 
131 131
     def test_update(self):
132
-        self.assertRaises(NotImplementedError,
133
-                          self.api.update, self.ctx, '', '')
132
+        fake_volume = {'fake': 'fake'}
133
+        self.mock_object(self.cinderclient.volumes, 'get',
134
+                         mock.Mock(return_value=fake_volume))
135
+        self.mock_object(self.cinderclient.volumes, 'update')
136
+        fake_volume_id = 'fake_volume'
137
+        fake_data = {'test': 'test'}
138
+
139
+        self.api.update(self.ctx, fake_volume_id, fake_data)
140
+
141
+        self.cinderclient.volumes.get.assert_called_once_with(fake_volume_id)
142
+        self.cinderclient.volumes.update.assert_called_once_with(fake_volume,
143
+                                                                 **fake_data)
134 144
 
135 145
     def test_reserve_volume(self):
136 146
         self.mock_object(self.cinderclient.volumes, 'reserve')

+ 7
- 1
manila/volume/cinder.py View File

@@ -26,6 +26,7 @@ from cinderclient.v2 import client as cinder_client
26 26
 from oslo_config import cfg
27 27
 from oslo_log import log
28 28
 
29
+import manila.context as ctxt
29 30
 from manila.db import base
30 31
 from manila import exception
31 32
 from manila.i18n import _
@@ -317,7 +318,12 @@ class API(base.Base):
317 318
 
318 319
     @translate_volume_exception
319 320
     def update(self, context, volume_id, fields):
320
-        raise NotImplementedError()
321
+        # Use Manila's context as far as Cinder's is restricted to update
322
+        # volumes.
323
+        manila_admin_context = ctxt.get_admin_context()
324
+        client = cinderclient(manila_admin_context)
325
+        item = client.volumes.get(volume_id)
326
+        client.volumes.update(item, **fields)
321 327
 
322 328
     def get_volume_encryption_metadata(self, context, volume_id):
323 329
         return cinderclient(context).volumes.get_encryption_metadata(volume_id)

Loading…
Cancel
Save