summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2015-03-19 04:53:53 +0000
committerGerrit Code Review <review@openstack.org>2015-03-19 04:53:53 +0000
commit20e590e8b3532291ddcbe01de3381d12329229bd (patch)
tree4a72e8df9cde42326f6205c991cd903485d13e93
parent725a9ced5551e326ef0004dc2205263b375c3f70 (diff)
parente1746d71378f5116e78070b886df00e3aed1d6a4 (diff)
Merge "Implement manage/unmanage support in generic driver"2015.1.0b3
-rwxr-xr-xcontrib/ci/post_test_hook.sh3
-rw-r--r--contrib/tempest/tempest/api/share/admin/test_share_manage.py145
-rw-r--r--contrib/tempest/tempest/config_share.py5
-rw-r--r--contrib/tempest/tempest/services/share/json/shares_client.py19
-rw-r--r--manila/api/contrib/share_manage.py16
-rw-r--r--manila/exception.py4
-rw-r--r--manila/share/api.py12
-rw-r--r--manila/share/drivers/generic.py150
-rw-r--r--manila/share/manager.py16
-rw-r--r--manila/tests/api/contrib/test_share_manage.py8
-rw-r--r--manila/tests/share/drivers/test_generic.py233
-rw-r--r--manila/tests/share/test_api.py21
-rw-r--r--manila/tests/share/test_manager.py16
-rw-r--r--manila/tests/volume/test_cinder.py14
-rw-r--r--manila/volume/cinder.py8
15 files changed, 599 insertions, 71 deletions
diff --git a/contrib/ci/post_test_hook.sh b/contrib/ci/post_test_hook.sh
index 18c283c..897f356 100755
--- a/contrib/ci/post_test_hook.sh
+++ b/contrib/ci/post_test_hook.sh
@@ -40,6 +40,9 @@ iniset $BASE/new/tempest/etc/tempest.conf share share_creation_retry_number 2
40# Suppress errors in cleanup of resources 40# Suppress errors in cleanup of resources
41iniset $BASE/new/tempest/etc/tempest.conf share suppress_errors_in_cleanup True 41iniset $BASE/new/tempest/etc/tempest.conf share suppress_errors_in_cleanup True
42 42
43# Enable manage/unmanage tests
44iniset $BASE/new/tempest/etc/tempest.conf share run_manage_unmanage_tests True
45
43# Define whether share drivers handle share servers or not. 46# Define whether share drivers handle share servers or not.
44# Requires defined config option 'driver_handles_share_servers'. 47# Requires defined config option 'driver_handles_share_servers'.
45MANILA_CONF=${MANILA_CONF:-/etc/manila/manila.conf} 48MANILA_CONF=${MANILA_CONF:-/etc/manila/manila.conf}
diff --git a/contrib/tempest/tempest/api/share/admin/test_share_manage.py b/contrib/tempest/tempest/api/share/admin/test_share_manage.py
new file mode 100644
index 0000000..8f99e9f
--- /dev/null
+++ b/contrib/tempest/tempest/api/share/admin/test_share_manage.py
@@ -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
16from tempest_lib import exceptions as lib_exc # noqa
17import testtools # noqa
18
19from tempest.api.share import base
20from tempest.common.utils import data_utils
21from tempest import config_share as config
22from tempest import test
23
24CONF = config.CONF
25
26
27class 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
144class ManageCIFSShareTest(ManageNFSShareTest):
145 protocol = 'cifs'
diff --git a/contrib/tempest/tempest/config_share.py b/contrib/tempest/tempest/config_share.py
index b58bacf..ce75769 100644
--- a/contrib/tempest/tempest/config_share.py
+++ b/contrib/tempest/tempest/config_share.py
@@ -108,6 +108,11 @@ ShareGroup = [
108 help="Whether to suppress errors with clean up operation " 108 help="Whether to suppress errors with clean up operation "
109 "or not. There are cases when we may want to skip " 109 "or not. There are cases when we may want to skip "
110 "such errors and catch only test errors."), 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
diff --git a/contrib/tempest/tempest/services/share/json/shares_client.py b/contrib/tempest/tempest/services/share/json/shares_client.py
index 95a3be6..7a9ee79 100644
--- a/contrib/tempest/tempest/services/share/json/shares_client.py
+++ b/contrib/tempest/tempest/services/share/json/shares_client.py
@@ -84,6 +84,23 @@ class SharesClient(service_client.ServiceClient):
84 def delete_share(self, share_id): 84 def delete_share(self, share_id):
85 return self.delete("shares/%s" % share_id) 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 def list_shares(self, detailed=False, params=None): 104 def list_shares(self, detailed=False, params=None):
88 """Get list of shares w/o filters.""" 105 """Get list of shares w/o filters."""
89 uri = 'shares/detail' if detailed else 'shares' 106 uri = 'shares/detail' if detailed else 'shares'
@@ -176,7 +193,7 @@ class SharesClient(service_client.ServiceClient):
176 share_status = body['status'] 193 share_status = body['status']
177 if share_status == status: 194 if share_status == status:
178 return 195 return
179 elif 'error' in share_status: 196 elif 'error' in share_status.lower():
180 raise share_exceptions.\ 197 raise share_exceptions.\
181 ShareBuildErrorException(share_id=share_id) 198 ShareBuildErrorException(share_id=share_id)
182 199
diff --git a/manila/api/contrib/share_manage.py b/manila/api/contrib/share_manage.py
index 66c6d42..0cd92d8 100644
--- a/manila/api/contrib/share_manage.py
+++ b/manila/api/contrib/share_manage.py
@@ -13,11 +13,11 @@
13# under the License. 13# under the License.
14 14
15import six 15import six
16import webob
17from webob import exc 16from webob import exc
18 17
19from manila.api import extensions 18from manila.api import extensions
20from manila.api.openstack import wsgi 19from manila.api.openstack import wsgi
20from manila.api.views import shares as share_views
21from manila import exception 21from manila import exception
22from manila.i18n import _ 22from manila.i18n import _
23from manila import share 23from manila import share
@@ -29,6 +29,9 @@ authorize = extensions.extension_authorizer('share', 'manage')
29 29
30 30
31class ShareManageController(wsgi.Controller): 31class ShareManageController(wsgi.Controller):
32
33 _view_builder_class = share_views.ViewBuilder
34
32 def __init__(self, *args, **kwargs): 35 def __init__(self, *args, **kwargs):
33 super(ShareManageController, self).__init__(*args, **kwargs) 36 super(ShareManageController, self).__init__(*args, **kwargs)
34 self.share_api = share.API() 37 self.share_api = share.API()
@@ -41,7 +44,7 @@ class ShareManageController(wsgi.Controller):
41 share = { 44 share = {
42 'host': share_data['service_host'], 45 'host': share_data['service_host'],
43 'export_location': share_data['export_path'], 46 'export_location': share_data['export_path'],
44 'share_proto': share_data['protocol'], 47 'share_proto': share_data['protocol'].upper(),
45 'share_type_id': share_data['share_type_id'], 48 'share_type_id': share_data['share_type_id'],
46 'display_name': share_data.get('display_name', ''), 49 'display_name': share_data.get('display_name', ''),
47 'display_description': share_data.get('display_description', ''), 50 'display_description': share_data.get('display_description', ''),
@@ -50,13 +53,13 @@ class ShareManageController(wsgi.Controller):
50 driver_options = share_data.get('driver_options', {}) 53 driver_options = share_data.get('driver_options', {})
51 54
52 try: 55 try:
53 self.share_api.manage(context, share, driver_options) 56 share_ref = self.share_api.manage(context, share, driver_options)
54 except exception.PolicyNotAuthorized as e: 57 except exception.PolicyNotAuthorized as e:
55 raise exc.HTTPForbidden(explanation=six.text_type(e)) 58 raise exc.HTTPForbidden(explanation=six.text_type(e))
56 except exception.ManilaException as e: 59 except exception.ManilaException as e:
57 raise exc.HTTPConflict(explanation=six.text_type(e)) 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 def _validate_manage_parameters(self, context, body): 64 def _validate_manage_parameters(self, context, body):
62 if not (body and self.is_valid_body(body, 'share')): 65 if not (body and self.is_valid_body(body, 'share')):
@@ -71,6 +74,9 @@ class ShareManageController(wsgi.Controller):
71 if parameter not in data: 74 if parameter not in data:
72 msg = _("Required parameter %s not found") % parameter 75 msg = _("Required parameter %s not found") % parameter
73 raise exc.HTTPUnprocessableEntity(explanation=msg) 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 if not share_utils.extract_host(data['service_host'], 'pool'): 81 if not share_utils.extract_host(data['service_host'], 'pool'):
76 msg = _("service_host parameter should contain pool.") 82 msg = _("service_host parameter should contain pool.")
@@ -114,4 +120,4 @@ class Share_manage(extensions.ExtensionDescriptor):
114 controller = ShareManageController() 120 controller = ShareManageController()
115 res = extensions.ResourceExtension(Share_manage.alias, 121 res = extensions.ResourceExtension(Share_manage.alias,
116 controller) 122 controller)
117 return [res] \ No newline at end of file 123 return [res]
diff --git a/manila/exception.py b/manila/exception.py
index 7adcc07..319637a 100644
--- a/manila/exception.py
+++ b/manila/exception.py
@@ -163,6 +163,10 @@ class InvalidUUID(Invalid):
163 message = _("Expected a uuid but received %(uuid)s.") 163 message = _("Expected a uuid but received %(uuid)s.")
164 164
165 165
166class InvalidDriverMode(Invalid):
167 message = _("Invalid driver mode: %(driver_mode)s.")
168
169
166class NotFound(ManilaException): 170class NotFound(ManilaException):
167 message = _("Resource could not be found.") 171 message = _("Resource could not be found.")
168 code = 404 172 code = 404
diff --git a/manila/share/api.py b/manila/share/api.py
index 14e0f9f..88f12db 100644
--- a/manila/share/api.py
+++ b/manila/share/api.py
@@ -230,11 +230,13 @@ class API(base.Base):
230 'project_id': context.project_id, 230 'project_id': context.project_id,
231 'status': constants.STATUS_MANAGING, 231 'status': constants.STATUS_MANAGING,
232 'scheduled_at': timeutils.utcnow(), 232 'scheduled_at': timeutils.utcnow(),
233 'deleted': False,
234 }) 233 })
235 234
236 retry_states = (constants.STATUS_MANAGE_ERROR, 235 LOG.debug("Manage: Found shares %s" % len(shares))
237 constants.STATUS_UNMANAGED) 236
237 retry_states = (constants.STATUS_MANAGE_ERROR,)
238
239 export_location = share_data.pop('export_location')
238 240
239 if len(shares) == 0: 241 if len(shares) == 0:
240 share = self.db.share_create(context, share_data) 242 share = self.db.share_create(context, share_data)
@@ -246,7 +248,11 @@ class API(base.Base):
246 msg = _("Share already exists.") 248 msg = _("Share already exists.")
247 raise exception.ManilaException(msg) 249 raise exception.ManilaException(msg)
248 250
251 self.db.share_export_locations_update(context, share['id'],
252 export_location)
253
249 self.share_rpcapi.manage_share(context, share, driver_options) 254 self.share_rpcapi.manage_share(context, share, driver_options)
255 return self.db.share_get(context, share['id'])
250 256
251 def unmanage(self, context, share): 257 def unmanage(self, context, share):
252 policy.check_policy(context, 'share', 'unmanage') 258 policy.check_policy(context, 'share', 'unmanage')
diff --git a/manila/share/drivers/generic.py b/manila/share/drivers/generic.py
index d75915f..68037db 100644
--- a/manila/share/drivers/generic.py
+++ b/manila/share/drivers/generic.py
@@ -24,6 +24,7 @@ from oslo_config import cfg
24from oslo_log import log 24from oslo_log import log
25from oslo_utils import excutils 25from oslo_utils import excutils
26from oslo_utils import importutils 26from oslo_utils import importutils
27from oslo_utils import strutils
27import six 28import six
28 29
29from manila.common import constants as const 30from manila.common import constants as const
@@ -35,6 +36,7 @@ from manila.i18n import _LE
35from manila.i18n import _LW 36from manila.i18n import _LW
36from manila.share import driver 37from manila.share import driver
37from manila.share.drivers import service_instance 38from manila.share.drivers import service_instance
39from manila.share import share_types
38from manila import utils 40from manila import utils
39from manila import volume 41from manila import volume
40 42
@@ -206,9 +208,8 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
206 volume['mountpoint']] 208 volume['mountpoint']]
207 self._ssh_exec(server_details, command) 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 """Checks whether volume already mounted or not.""" 212 """Checks whether volume already mounted or not."""
211 mount_path = self._get_mount_path(share)
212 log_data = { 213 log_data = {
213 'mount_path': mount_path, 214 'mount_path': mount_path,
214 'server_id': server_details['instance_id'], 215 'server_id': server_details['instance_id'],
@@ -274,7 +275,8 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
274 'server': server_details['instance_id'], 275 'server': server_details['instance_id'],
275 } 276 }
276 try: 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 LOG.debug("Mounting '%(dev)s' to path '%(path)s' on " 280 LOG.debug("Mounting '%(dev)s' to path '%(path)s' on "
279 "server '%(server)s'.", log_data) 281 "server '%(server)s'.", log_data)
280 mount_cmd = ['sudo mkdir -p', mount_path, '&&'] 282 mount_cmd = ['sudo mkdir -p', mount_path, '&&']
@@ -303,7 +305,7 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
303 'path': mount_path, 305 'path': mount_path,
304 'server': server_details['instance_id'], 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 LOG.debug("Unmounting path '%(path)s' on server " 309 LOG.debug("Unmounting path '%(path)s' on server "
308 "'%(server)s'.", log_data) 310 "'%(server)s'.", log_data)
309 unmount_cmd = ['sudo umount', mount_path, '&& sudo rmdir', 311 unmount_cmd = ['sudo umount', mount_path, '&& sudo rmdir',
@@ -354,24 +356,28 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
354 self.configuration.max_time_to_attach) 356 self.configuration.max_time_to_attach)
355 return do_attach(volume) 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 def _get_volume(self, context, share_id): 362 def _get_volume(self, context, share_id):
358 """Finds volume, associated to the specific share.""" 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 search_opts = {'name': volume_name} 365 search_opts = {'name': volume_name}
361 if context.is_admin: 366 if context.is_admin:
362 search_opts['all_tenants'] = True 367 search_opts['all_tenants'] = True
363 volumes_list = self.volume_api.get_all(context, search_opts) 368 volumes_list = self.volume_api.get_all(context, search_opts)
364 volume = None
365 if len(volumes_list) == 1: 369 if len(volumes_list) == 1:
366 volume = volumes_list[0] 370 return volumes_list[0]
367 elif len(volumes_list) > 1: 371 elif len(volumes_list) > 1:
368 LOG.error( 372 LOG.error(
369 _LE("Expected only one volume in volume list with name " 373 _LE("Expected only one volume in volume list with name "
370 "'%(name)s', but got more than one in a result - " 374 "'%(name)s', but got more than one in a result - "
371 "'%(result)s'."), { 375 "'%(result)s'."), {
372 'name': volume_name, 'result': volumes_list}) 376 'name': volume_name, 'result': volumes_list})
373 raise exception.ManilaException(_('Error. Ambiguous volumes')) 377 raise exception.ManilaException(
374 return volume 378 _("Error. Ambiguous volumes for name '%s'") % volume_name)
379 else:
380 raise exception.VolumeNotFound(volume_id=volume_name)
375 381
376 def _get_volume_snapshot(self, context, snapshot_id): 382 def _get_volume_snapshot(self, context, snapshot_id):
377 """Find volume snapshot associated to the specific share snapshot.""" 383 """Find volume snapshot associated to the specific share snapshot."""
@@ -633,6 +639,107 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
633 self.service_instance_manager.delete_service_instance( 639 self.service_instance_manager.delete_service_instance(
634 self.admin_context, server_details) 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
637class NASHelperBase(object): 744class NASHelperBase(object):
638 """Interface to work with share.""" 745 """Interface to work with share."""
@@ -662,6 +769,10 @@ class NASHelperBase(object):
662 """Deny access to the host.""" 769 """Deny access to the host."""
663 raise NotImplementedError() 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
666def nfs_synchronized(f): 777def nfs_synchronized(f):
667 778
@@ -745,6 +856,9 @@ class NFSHelper(NASHelperBase):
745 ] 856 ]
746 self._ssh_exec(server, sync_cmd) 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
749class CIFSHelper(NASHelperBase): 863class CIFSHelper(NASHelperBase):
750 """Manage shares in samba server by net conf tool. 864 """Manage shares in samba server by net conf tool.
@@ -855,3 +969,21 @@ class CIFSHelper(NASHelperBase):
855 value = "\"" + ' '.join(hosts) + "\"" 969 value = "\"" + ' '.join(hosts) + "\""
856 self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name, 970 self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name,
857 '\"hosts allow\"', value]) 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()
diff --git a/manila/share/manager.py b/manila/share/manager.py
index 0e16998..1db6903 100644
--- a/manila/share/manager.py
+++ b/manila/share/manager.py
@@ -340,8 +340,7 @@ class ShareManager(manager.SchedulerDependentManager):
340 raise exception.InvalidShare(reason=msg) 340 raise exception.InvalidShare(reason=msg)
341 341
342 share_update = ( 342 share_update = (
343 self.driver.manage_existing(share_ref, driver_options) or {} 343 self.driver.manage_existing(share_ref, driver_options) or {})
344 )
345 344
346 if not share_update.get('size'): 345 if not share_update.get('size'):
347 msg = _("Driver cannot calculate share size.") 346 msg = _("Driver cannot calculate share size.")
@@ -349,19 +348,22 @@ class ShareManager(manager.SchedulerDependentManager):
349 348
350 self._update_quota_usages(context, project_id, { 349 self._update_quota_usages(context, project_id, {
351 "shares": 1, 350 "shares": 1,
352 "gigabytes": share_update['size'] 351 "gigabytes": share_update['size'],
353 }) 352 })
354 353
355 share_update.update({ 354 share_update.update({
356 'status': 'available', 355 'status': 'available',
357 'launched_at': timeutils.utcnow() 356 'launched_at': timeutils.utcnow(),
358 }) 357 })
359 self.db.share_update(context, share_id, share_update) 358 self.db.share_update(context, share_id, share_update)
360
361 except Exception as e: 359 except Exception as e:
362 LOG.error(_LW("Manage share failed: %s"), six.text_type(e)) 360 LOG.error(_LW("Manage share failed: %s"), six.text_type(e))
363 self.db.share_update(context, share_id, 361 # NOTE(vponomaryov): set size as 1 because design expects size
364 {'status': constants.STATUS_MANAGE_ERROR}) 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 def _update_quota_usages(self, context, project_id, usages): 368 def _update_quota_usages(self, context, project_id, usages):
367 user_id = context.user_id 369 user_id = context.user_id
diff --git a/manila/tests/api/contrib/test_share_manage.py b/manila/tests/api/contrib/test_share_manage.py
index ecb856c..0245bc9 100644
--- a/manila/tests/api/contrib/test_share_manage.py
+++ b/manila/tests/api/contrib/test_share_manage.py
@@ -141,12 +141,14 @@ class ShareManageTest(test.TestCase):
141 def test_share_manage(self): 141 def test_share_manage(self):
142 body = get_fake_manage_body() 142 body = get_fake_manage_body()
143 self._setup_manage_mocks() 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 share = { 148 share = {
147 'host': body['share']['service_host'], 149 'host': body['share']['service_host'],
148 'export_location': body['share']['export_path'], 150 'export_location': body['share']['export_path'],
149 'share_proto': body['share']['protocol'], 151 'share_proto': body['share']['protocol'].upper(),
150 'share_type_id': 'fake', 152 'share_type_id': 'fake',
151 'display_name': body['share']['display_name'], 153 'display_name': body['share']['display_name'],
152 'display_description': body['share']['display_description'], 154 'display_description': body['share']['display_description'],
@@ -156,7 +158,7 @@ class ShareManageTest(test.TestCase):
156 158
157 share_api.API.manage.assert_called_once_with( 159 share_api.API.manage.assert_called_once_with(
158 mock.ANY, share, {}) 160 mock.ANY, share, {})
159 self.assertEqual(202, actual_result.status_int) 161 self.assertIsNotNone(actual_result)
160 162
161 def test_wrong_permissions(self): 163 def test_wrong_permissions(self):
162 body = get_fake_manage_body() 164 body = get_fake_manage_body()
diff --git a/manila/tests/share/drivers/test_generic.py b/manila/tests/share/drivers/test_generic.py
index a1ff924..e640692 100644
--- a/manila/tests/share/drivers/test_generic.py
+++ b/manila/tests/share/drivers/test_generic.py
@@ -29,6 +29,7 @@ from manila import context
29from manila import exception 29from manila import exception
30import manila.share.configuration 30import manila.share.configuration
31from manila.share.drivers import generic 31from manila.share.drivers import generic
32from manila.share import share_types
32from manila import test 33from manila import test
33from manila.tests import fake_compute 34from manila.tests import fake_compute
34from manila.tests import fake_service_instance 35from manila.tests import fake_service_instance
@@ -41,6 +42,16 @@ from manila import volume
41CONF = cfg.CONF 42CONF = cfg.CONF
42 43
43 44
45def 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@ddt.ddt 55@ddt.ddt
45class GenericShareDriverTestCase(test.TestCase): 56class GenericShareDriverTestCase(test.TestCase):
46 """Tests GenericShareDriver.""" 57 """Tests GenericShareDriver."""
@@ -185,21 +196,18 @@ class GenericShareDriverTestCase(test.TestCase):
185 196
186 def test_mount_device_not_present(self): 197 def test_mount_device_not_present(self):
187 server = {'instance_id': 'fake_server_id'} 198 server = {'instance_id': 'fake_server_id'}
188 mount_path = '/fake/mount/path' 199 mount_path = self._driver._get_mount_path(self.share)
189 volume = {'mountpoint': 'fake_mount_point'} 200 volume = {'mountpoint': 'fake_mount_point'}
190 self.mock_object(self._driver, '_is_device_mounted', 201 self.mock_object(self._driver, '_is_device_mounted',
191 mock.Mock(return_value=False)) 202 mock.Mock(return_value=False))
192 self.mock_object(self._driver, '_sync_mount_temp_and_perm_files') 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 self.mock_object(self._driver, '_ssh_exec', 204 self.mock_object(self._driver, '_ssh_exec',
196 mock.Mock(return_value=('', ''))) 205 mock.Mock(return_value=('', '')))
197 206
198 self._driver._mount_device(self.share, server, volume) 207 self._driver._mount_device(self.share, server, volume)
199 208
200 self._driver._get_mount_path.assert_called_once_with(self.share)
201 self._driver._is_device_mounted.assert_called_once_with( 209 self._driver._is_device_mounted.assert_called_once_with(
202 self.share, server, volume) 210 mount_path, server, volume)
203 self._driver._sync_mount_temp_and_perm_files.assert_called_once_with( 211 self._driver._sync_mount_temp_and_perm_files.assert_called_once_with(
204 server) 212 server)
205 self._driver._ssh_exec.assert_called_once_with( 213 self._driver._ssh_exec.assert_called_once_with(
@@ -222,13 +230,12 @@ class GenericShareDriverTestCase(test.TestCase):
222 230
223 self._driver._get_mount_path.assert_called_once_with(self.share) 231 self._driver._get_mount_path.assert_called_once_with(self.share)
224 self._driver._is_device_mounted.assert_called_once_with( 232 self._driver._is_device_mounted.assert_called_once_with(
225 self.share, self.server, volume) 233 mount_path, self.server, volume)
226 generic.LOG.warning.assert_called_once_with(mock.ANY, mock.ANY) 234 generic.LOG.warning.assert_called_once_with(mock.ANY, mock.ANY)
227 235
228 def test_mount_device_exception_raised(self): 236 def test_mount_device_exception_raised(self):
229 volume = {'mountpoint': 'fake_mount_point'} 237 volume = {'mountpoint': 'fake_mount_point'}
230 self.mock_object(self._driver, '_get_mount_path', 238
231 mock.Mock(return_value='fake'))
232 self.mock_object( 239 self.mock_object(
233 self._driver, '_is_device_mounted', 240 self._driver, '_is_device_mounted',
234 mock.Mock(side_effect=exception.ProcessExecutionError)) 241 mock.Mock(side_effect=exception.ProcessExecutionError))
@@ -240,9 +247,8 @@ class GenericShareDriverTestCase(test.TestCase):
240 self.server, 247 self.server,
241 volume, 248 volume,
242 ) 249 )
243 self._driver._get_mount_path.assert_called_once_with(self.share)
244 self._driver._is_device_mounted.assert_called_once_with( 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 def test_unmount_device_present(self): 253 def test_unmount_device_present(self):
248 mount_path = '/fake/mount/path' 254 mount_path = '/fake/mount/path'
@@ -258,7 +264,7 @@ class GenericShareDriverTestCase(test.TestCase):
258 264
259 self._driver._get_mount_path.assert_called_once_with(self.share) 265 self._driver._get_mount_path.assert_called_once_with(self.share)
260 self._driver._is_device_mounted.assert_called_once_with( 266 self._driver._is_device_mounted.assert_called_once_with(
261 self.share, self.server) 267 mount_path, self.server)
262 self._driver._sync_mount_temp_and_perm_files.assert_called_once_with( 268 self._driver._sync_mount_temp_and_perm_files.assert_called_once_with(
263 self.server) 269 self.server)
264 self._driver._ssh_exec.assert_called_once_with( 270 self._driver._ssh_exec.assert_called_once_with(
@@ -278,7 +284,7 @@ class GenericShareDriverTestCase(test.TestCase):
278 284
279 self._driver._get_mount_path.assert_called_once_with(self.share) 285 self._driver._get_mount_path.assert_called_once_with(self.share)
280 self._driver._is_device_mounted.assert_called_once_with( 286 self._driver._is_device_mounted.assert_called_once_with(
281 self.share, self.server) 287 mount_path, self.server)
282 generic.LOG.warning.assert_called_once_with(mock.ANY, mock.ANY) 288 generic.LOG.warning.assert_called_once_with(mock.ANY, mock.ANY)
283 289
284 def test_is_device_mounted_true(self): 290 def test_is_device_mounted_true(self):
@@ -288,13 +294,10 @@ class GenericShareDriverTestCase(test.TestCase):
288 'path': mount_path} 294 'path': mount_path}
289 self.mock_object(self._driver, '_ssh_exec', 295 self.mock_object(self._driver, '_ssh_exec',
290 mock.Mock(return_value=(mounts, ''))) 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 result = self._driver._is_device_mounted( 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 self._driver._ssh_exec.assert_called_once_with( 301 self._driver._ssh_exec.assert_called_once_with(
299 self.server, ['sudo', 'mount']) 302 self.server, ['sudo', 'mount'])
300 self.assertEqual(result, True) 303 self.assertEqual(result, True)
@@ -304,12 +307,9 @@ class GenericShareDriverTestCase(test.TestCase):
304 mounts = "/fake/dev/path on %(path)s type fake" % {'path': mount_path} 307 mounts = "/fake/dev/path on %(path)s type fake" % {'path': mount_path}
305 self.mock_object(self._driver, '_ssh_exec', 308 self.mock_object(self._driver, '_ssh_exec',
306 mock.Mock(return_value=(mounts, ''))) 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 self._driver._ssh_exec.assert_called_once_with( 313 self._driver._ssh_exec.assert_called_once_with(
314 self.server, ['sudo', 'mount']) 314 self.server, ['sudo', 'mount'])
315 self.assertEqual(result, True) 315 self.assertEqual(result, True)
@@ -321,13 +321,10 @@ class GenericShareDriverTestCase(test.TestCase):
321 'path': mount_path} 321 'path': mount_path}
322 self.mock_object(self._driver, '_ssh_exec', 322 self.mock_object(self._driver, '_ssh_exec',
323 mock.Mock(return_value=(mounts, ''))) 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 result = self._driver._is_device_mounted( 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 self._driver._ssh_exec.assert_called_once_with( 328 self._driver._ssh_exec.assert_called_once_with(
332 self.server, ['sudo', 'mount']) 329 self.server, ['sudo', 'mount'])
333 self.assertEqual(result, False) 330 self.assertEqual(result, False)
@@ -340,9 +337,8 @@ class GenericShareDriverTestCase(test.TestCase):
340 self.mock_object(self._driver, '_get_mount_path', 337 self.mock_object(self._driver, '_get_mount_path',
341 mock.Mock(return_value=mount_path)) 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 self._driver._ssh_exec.assert_called_once_with( 342 self._driver._ssh_exec.assert_called_once_with(
347 self.server, ['sudo', 'mount']) 343 self.server, ['sudo', 'mount'])
348 self.assertEqual(result, False) 344 self.assertEqual(result, False)
@@ -467,8 +463,12 @@ class GenericShareDriverTestCase(test.TestCase):
467 self._driver.configuration.volume_name_template % self.share['id']) 463 self._driver.configuration.volume_name_template % self.share['id'])
468 self.mock_object(self._driver.volume_api, 'get_all', 464 self.mock_object(self._driver.volume_api, 'get_all',
469 mock.Mock(return_value=[])) 465 mock.Mock(return_value=[]))
470 result = self._driver._get_volume(self._context, self.share['id']) 466
471 self.assertEqual(result, None) 467 self.assertRaises(
468 exception.VolumeNotFound,
469 self._driver._get_volume,
470 self._context, self.share['id'])
471
472 self._driver.volume_api.get_all.assert_called_once_with( 472 self._driver.volume_api.get_all.assert_called_once_with(
473 self._context, {'all_tenants': True, 'name': vol_name}) 473 self._context, {'all_tenants': True, 'name': vol_name})
474 474
@@ -1011,6 +1011,183 @@ class GenericShareDriverTestCase(test.TestCase):
1011 self.assertEqual(True, result['driver_handles_share_servers']) 1011 self.assertEqual(True, result['driver_handles_share_servers'])
1012 self.assertEqual('Open Source', result['vendor_name']) 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@generic.ensure_server 1192@generic.ensure_server
1016def fake(driver_instance, context, share_server=None): 1193def fake(driver_instance, context, share_server=None):
diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py
index 704a807..6eda46f 100644
--- a/manila/tests/share/test_api.py
+++ b/manila/tests/share/test_api.py
@@ -550,6 +550,9 @@ class ShareAPITestCase(test.TestCase):
550 status='creating') 550 status='creating')
551 self.mock_object(db_driver, 'share_create', 551 self.mock_object(db_driver, 'share_create',
552 mock.Mock(return_value=share)) 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 self.mock_object(self.api, 'get_all', mock.Mock(return_value=[])) 556 self.mock_object(self.api, 'get_all', mock.Mock(return_value=[]))
554 557
555 self.api.manage(self.context, 558 self.api.manage(self.context,
@@ -561,17 +564,20 @@ class ShareAPITestCase(test.TestCase):
561 'project_id': self.context.project_id, 564 'project_id': self.context.project_id,
562 'status': constants.STATUS_MANAGING, 565 'status': constants.STATUS_MANAGING,
563 'scheduled_at': date, 566 'scheduled_at': date,
564 'deleted': False,
565 }) 567 })
566 568
569 export_location = share_data.pop('export_location')
567 self.api.get_all.assert_called_once_with(self.context, mock.ANY) 570 self.api.get_all.assert_called_once_with(self.context, mock.ANY)
568 db_driver.share_create.assert_called_once_with(self.context, 571 db_driver.share_create.assert_called_once_with(self.context,
569 share_data) 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 self.share_rpcapi.manage_share.assert_called_once_with( 577 self.share_rpcapi.manage_share.assert_called_once_with(
571 self.context, share, driver_options) 578 self.context, share, driver_options)
572 579
573 @ddt.data([{'id': 'fake', 'status': constants.STATUS_MANAGE_ERROR}], 580 @ddt.data([{'id': 'fake', 'status': constants.STATUS_MANAGE_ERROR}])
574 [{'id': 'fake', 'status': constants.STATUS_UNMANAGED}],)
575 def test_manage_retry(self, shares): 581 def test_manage_retry(self, shares):
576 share_data = { 582 share_data = {
577 'host': 'fake', 583 'host': 'fake',
@@ -579,8 +585,12 @@ class ShareAPITestCase(test.TestCase):
579 'share_proto': 'fake', 585 'share_proto': 'fake',
580 } 586 }
581 driver_options = {} 587 driver_options = {}
588 share = fake_share('fakeid')
582 self.mock_object(db_driver, 'share_update', 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 self.mock_object(self.api, 'get_all', 594 self.mock_object(self.api, 'get_all',
585 mock.Mock(return_value=shares)) 595 mock.Mock(return_value=shares))
586 596
@@ -592,6 +602,9 @@ class ShareAPITestCase(test.TestCase):
592 self.context, 'fake', mock.ANY) 602 self.context, 'fake', mock.ANY)
593 self.share_rpcapi.manage_share.assert_called_once_with( 603 self.share_rpcapi.manage_share.assert_called_once_with(
594 self.context, mock.ANY, driver_options) 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 def test_manage_duplicate(self): 609 def test_manage_duplicate(self):
597 share_data = { 610 share_data = {
diff --git a/manila/tests/share/test_manager.py b/manila/tests/share/test_manager.py
index ddd0fcf..b9254c8 100644
--- a/manila/tests/share/test_manager.py
+++ b/manila/tests/share/test_manager.py
@@ -710,8 +710,8 @@ class ShareManagerTestCase(test.TestCase):
710 self.share_manager.manage_share(self.context, share_id, {}) 710 self.share_manager.manage_share(self.context, share_id, {})
711 711
712 self.share_manager.db.share_update.assert_called_once_with( 712 self.share_manager.db.share_update.assert_called_once_with(
713 mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR} 713 mock.ANY, share_id,
714 ) 714 {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
715 715
716 def test_manage_share_driver_exception(self): 716 def test_manage_share_driver_exception(self):
717 self.mock_object(self.share_manager, 'driver', mock.Mock()) 717 self.mock_object(self.share_manager, 'driver', mock.Mock())
@@ -729,8 +729,8 @@ class ShareManagerTestCase(test.TestCase):
729 assert_called_once_with(mock.ANY, driver_options) 729 assert_called_once_with(mock.ANY, driver_options)
730 730
731 self.share_manager.db.share_update.assert_called_once_with( 731 self.share_manager.db.share_update.assert_called_once_with(
732 mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR} 732 mock.ANY, share_id,
733 ) 733 {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
734 734
735 def test_manage_share_invalid_size(self): 735 def test_manage_share_invalid_size(self):
736 self.mock_object(self.share_manager, 'driver') 736 self.mock_object(self.share_manager, 'driver')
@@ -749,8 +749,8 @@ class ShareManagerTestCase(test.TestCase):
749 assert_called_once_with(mock.ANY, driver_options) 749 assert_called_once_with(mock.ANY, driver_options)
750 750
751 self.share_manager.db.share_update.assert_called_once_with( 751 self.share_manager.db.share_update.assert_called_once_with(
752 mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR} 752 mock.ANY, share_id,
753 ) 753 {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
754 754
755 def test_manage_share_quota_error(self): 755 def test_manage_share_quota_error(self):
756 self.mock_object(self.share_manager, 'driver') 756 self.mock_object(self.share_manager, 'driver')
@@ -771,8 +771,8 @@ class ShareManagerTestCase(test.TestCase):
771 assert_called_once_with(mock.ANY, driver_options) 771 assert_called_once_with(mock.ANY, driver_options)
772 772
773 self.share_manager.db.share_update.assert_called_once_with( 773 self.share_manager.db.share_update.assert_called_once_with(
774 mock.ANY, share_id, {'status': constants.STATUS_MANAGE_ERROR} 774 mock.ANY, share_id,
775 ) 775 {'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
776 776
777 @ddt.data({'size': 1}, 777 @ddt.data({'size': 1},
778 {'size': 1, 'name': 'fake'}) 778 {'size': 1, 'name': 'fake'})
diff --git a/manila/tests/volume/test_cinder.py b/manila/tests/volume/test_cinder.py
index 27a8d2d..908fc3a 100644
--- a/manila/tests/volume/test_cinder.py
+++ b/manila/tests/volume/test_cinder.py
@@ -129,8 +129,18 @@ class CinderApiTestCase(test.TestCase):
129 self.assertIsNone(self.api.check_detach(self.ctx, volume)) 129 self.assertIsNone(self.api.check_detach(self.ctx, volume))
130 130
131 def test_update(self): 131 def test_update(self):
132 self.assertRaises(NotImplementedError, 132 fake_volume = {'fake': 'fake'}
133 self.api.update, self.ctx, '', '') 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 def test_reserve_volume(self): 145 def test_reserve_volume(self):
136 self.mock_object(self.cinderclient.volumes, 'reserve') 146 self.mock_object(self.cinderclient.volumes, 'reserve')
diff --git a/manila/volume/cinder.py b/manila/volume/cinder.py
index f9c671d..dafd429 100644
--- a/manila/volume/cinder.py
+++ b/manila/volume/cinder.py
@@ -26,6 +26,7 @@ from cinderclient.v2 import client as cinder_client
26from oslo_config import cfg 26from oslo_config import cfg
27from oslo_log import log 27from oslo_log import log
28 28
29import manila.context as ctxt
29from manila.db import base 30from manila.db import base
30from manila import exception 31from manila import exception
31from manila.i18n import _ 32from manila.i18n import _
@@ -317,7 +318,12 @@ class API(base.Base):
317 318
318 @translate_volume_exception 319 @translate_volume_exception
319 def update(self, context, volume_id, fields): 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 def get_volume_encryption_metadata(self, context, volume_id): 328 def get_volume_encryption_metadata(self, context, volume_id):
323 return cinderclient(context).volumes.get_encryption_metadata(volume_id) 329 return cinderclient(context).volumes.get_encryption_metadata(volume_id)