Coverage for manila/tests/share/drivers/nexenta/ns4/test_nexenta_nas.py: 97%
262 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright 2016 Nexenta Systems, 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.
16import base64
17import json
18from unittest import mock
20from oslo_serialization import jsonutils
21from oslo_utils import units
23from manila import context
24from manila import exception
25from manila.share import configuration as conf
26from manila.share.drivers.nexenta.ns4 import nexenta_nas
27from manila import test
29PATH_TO_RPC = 'requests.post'
30CODE = mock.PropertyMock(return_value=200)
33class FakeResponse(object):
35 def __init__(self, response={}):
36 self.content = json.dumps(response)
37 super(FakeResponse, self).__init__()
39 def close(self):
40 pass
43class RequestParams(object):
44 def __init__(self, scheme, host, port, path, user, password):
45 self.scheme = scheme.lower()
46 self.host = host
47 self.port = port
48 self.path = path
49 self.user = user
50 self.password = password
52 @property
53 def url(self):
54 return '%s://%s:%s%s' % (self.scheme, self.host, self.port, self.path)
56 @property
57 def headers(self):
58 auth = base64.b64encode(
59 ('%s:%s' % (self.user, self.password)).encode('utf-8'))
60 headers = {
61 'Content-Type': 'application/json',
62 'Authorization': 'Basic %s' % auth,
63 }
64 return headers
66 def build_post_args(self, obj, method, *args):
67 data = jsonutils.dumps({
68 'object': obj,
69 'method': method,
70 'params': args,
71 })
72 return data
75class TestNexentaNasDriver(test.TestCase):
77 def _get_share_path(self, share_name):
78 return '%s/%s/%s' % (self.volume, self.share, share_name)
80 def setUp(self):
81 def _safe_get(opt):
82 return getattr(self.cfg, opt)
84 self.cfg = mock.Mock(spec=conf.Configuration)
85 self.cfg.nexenta_nas_host = '1.1.1.1'
86 super(TestNexentaNasDriver, self).setUp()
88 self.ctx = context.get_admin_context()
89 self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
90 self.cfg.nexenta_rest_port = 1000
91 self.cfg.reserved_share_percentage = 0
92 self.cfg.reserved_share_from_snapshot_percentage = 0
93 self.cfg.reserved_share_extend_percentage = 0
94 self.cfg.max_over_subscription_ratio = 0
95 self.cfg.nexenta_rest_protocol = 'auto'
96 self.cfg.nexenta_volume = 'volume'
97 self.cfg.nexenta_nfs_share = 'nfs_share'
98 self.cfg.nexenta_user = 'user'
99 self.cfg.nexenta_password = 'password'
100 self.cfg.nexenta_thin_provisioning = False
101 self.cfg.enabled_share_protocols = 'NFS'
102 self.cfg.nexenta_mount_point_base = '$state_path/mnt'
103 self.cfg.share_backend_name = 'NexentaStor'
104 self.cfg.nexenta_dataset_compression = 'on'
105 self.cfg.nexenta_smb = 'on'
106 self.cfg.nexenta_nfs = 'on'
107 self.cfg.nexenta_dataset_dedupe = 'on'
109 self.cfg.network_config_group = 'DEFAULT'
110 self.cfg.admin_network_config_group = (
111 'fake_admin_network_config_group')
112 self.cfg.driver_handles_share_servers = False
114 self.request_params = RequestParams(
115 'http', self.cfg.nexenta_nas_host, self.cfg.nexenta_rest_port,
116 '/rest/nms/', self.cfg.nexenta_user, self.cfg.nexenta_password)
118 self.drv = nexenta_nas.NexentaNasDriver(configuration=self.cfg)
119 self.drv.do_setup(self.ctx)
121 self.volume = self.cfg.nexenta_volume
122 self.share = self.cfg.nexenta_nfs_share
124 @mock.patch(PATH_TO_RPC)
125 def test_check_for_setup_error__volume_doesnt_exist(self, post):
126 post.return_value = FakeResponse()
128 self.assertRaises(
129 exception.NexentaException, self.drv.check_for_setup_error)
131 @mock.patch(PATH_TO_RPC)
132 def test_check_for_setup_error__folder_doesnt_exist(self, post):
133 folder = '%s/%s' % (self.volume, self.share)
134 create_folder_props = {
135 'recordsize': '4K',
136 'quota': '1G',
137 'compression': self.cfg.nexenta_dataset_compression,
138 'sharesmb': self.cfg.nexenta_smb,
139 'sharenfs': self.cfg.nexenta_nfs,
140 }
142 share_opts = {
143 'read_write': '*',
144 'read_only': '',
145 'root': 'nobody',
146 'extra_options': 'anon=0',
147 'recursive': 'true',
148 'anonymous_rw': 'true',
149 }
151 def my_side_effect(*args, **kwargs):
152 if kwargs['data'] == self.request_params.build_post_args(
153 'volume', 'object_exists', self.volume):
154 return FakeResponse({'result': 'OK'})
155 elif kwargs['data'] == self.request_params.build_post_args(
156 'folder', 'object_exists', folder):
157 return FakeResponse()
158 elif kwargs['data'] == self.request_params.build_post_args( 158 ↛ 161line 158 didn't jump to line 161 because the condition on line 158 was never true
159 'folder', 'create_with_props', self.volume, self.share,
160 create_folder_props):
161 return FakeResponse()
162 elif kwargs['data'] == self.request_params.build_post_args( 162 ↛ 165line 162 didn't jump to line 165 because the condition on line 162 was never true
163 'netstorsvc', 'share_folder',
164 'svc:/network/nfs/server:default', folder, share_opts):
165 return FakeResponse()
166 else:
167 raise exception.ManilaException('Unexpected request')
168 post.side_effect = my_side_effect
170 self.assertRaises(
171 exception.ManilaException, self.drv.check_for_setup_error)
172 post.assert_any_call(
173 self.request_params.url, data=self.request_params.build_post_args(
174 'volume', 'object_exists', self.volume),
175 headers=self.request_params.headers,
176 timeout=60)
177 post.assert_any_call(
178 self.request_params.url, data=self.request_params.build_post_args(
179 'folder', 'object_exists', folder),
180 headers=self.request_params.headers,
181 timeout=60)
183 @mock.patch(PATH_TO_RPC)
184 def test_create_share(self, post):
185 share = {
186 'name': 'share',
187 'size': 1,
188 'share_proto': self.cfg.enabled_share_protocols
189 }
190 self.cfg.nexenta_thin_provisioning = False
191 path = '%s/%s/%s' % (self.volume, self.share, share['name'])
192 location = {'path': '%s:/volumes/%s' % (
193 self.cfg.nexenta_nas_host, path)}
194 post.return_value = FakeResponse()
196 self.assertEqual([location],
197 self.drv.create_share(self.ctx, share))
199 @mock.patch(PATH_TO_RPC)
200 def test_create_share__wrong_proto(self, post):
201 share = {
202 'name': 'share',
203 'size': 1,
204 'share_proto': 'A_VERY_WRONG_PROTO'
205 }
206 post.return_value = FakeResponse()
208 self.assertRaises(exception.InvalidShare, self.drv.create_share,
209 self.ctx, share)
211 @mock.patch(PATH_TO_RPC)
212 def test_create_share__thin_provisioning(self, post):
213 share = {'name': 'share', 'size': 1,
214 'share_proto': self.cfg.enabled_share_protocols}
215 create_folder_props = {
216 'recordsize': '4K',
217 'quota': '1G',
218 'compression': self.cfg.nexenta_dataset_compression,
219 }
220 parent_path = '%s/%s' % (self.volume, self.share)
221 post.return_value = FakeResponse()
222 self.cfg.nexenta_thin_provisioning = True
224 self.drv.create_share(self.ctx, share)
226 post.assert_called_with(
227 self.request_params.url,
228 data=self.request_params.build_post_args(
229 'folder',
230 'create_with_props',
231 parent_path,
232 share['name'],
233 create_folder_props),
234 headers=self.request_params.headers,
235 timeout=60)
237 @mock.patch(PATH_TO_RPC)
238 def test_create_share__thick_provisioning(self, post):
239 share = {
240 'name': 'share',
241 'size': 1,
242 'share_proto': self.cfg.enabled_share_protocols
243 }
244 quota = '%sG' % share['size']
245 create_folder_props = {
246 'recordsize': '4K',
247 'quota': quota,
248 'compression': self.cfg.nexenta_dataset_compression,
249 'reservation': quota,
250 }
251 parent_path = '%s/%s' % (self.volume, self.share)
252 post.return_value = FakeResponse()
253 self.cfg.nexenta_thin_provisioning = False
255 self.drv.create_share(self.ctx, share)
257 post.assert_called_with(
258 self.request_params.url,
259 data=self.request_params.build_post_args(
260 'folder',
261 'create_with_props',
262 parent_path,
263 share['name'],
264 create_folder_props),
265 headers=self.request_params.headers,
266 timeout=60)
268 @mock.patch(PATH_TO_RPC)
269 def test_create_share_from_snapshot(self, post):
270 share = {
271 'name': 'share',
272 'size': 1,
273 'share_proto': self.cfg.enabled_share_protocols
274 }
275 snapshot = {'name': 'sn1', 'share_name': share['name']}
276 post.return_value = FakeResponse()
277 path = '%s/%s/%s' % (self.volume, self.share, share['name'])
278 location = {'path': '%s:/volumes/%s' % (
279 self.cfg.nexenta_nas_host, path)}
280 snapshot_name = '%s/%s/%s@%s' % (
281 self.volume, self.share, snapshot['share_name'], snapshot['name'])
283 self.assertEqual([location], self.drv.create_share_from_snapshot(
284 self.ctx, share, snapshot))
285 post.assert_any_call(
286 self.request_params.url,
287 data=self.request_params.build_post_args(
288 'folder',
289 'clone',
290 snapshot_name,
291 '%s/%s/%s' % (self.volume, self.share, share['name'])),
292 headers=self.request_params.headers,
293 timeout=60)
295 @mock.patch(PATH_TO_RPC)
296 def test_delete_share(self, post):
297 share = {
298 'name': 'share',
299 'size': 1,
300 'share_proto': self.cfg.enabled_share_protocols
301 }
302 post.return_value = FakeResponse()
303 folder = '%s/%s/%s' % (self.volume, self.share, share['name'])
305 self.drv.delete_share(self.ctx, share)
307 post.assert_any_call(
308 self.request_params.url,
309 data=self.request_params.build_post_args(
310 'folder',
311 'destroy',
312 folder.strip(),
313 '-r'),
314 headers=self.request_params.headers,
315 timeout=60)
317 @mock.patch(PATH_TO_RPC)
318 def test_delete_share__exists_error(self, post):
319 share = {
320 'name': 'share',
321 'size': 1,
322 'share_proto': self.cfg.enabled_share_protocols
323 }
324 post.return_value = FakeResponse()
325 post.side_effect = exception.NexentaException('does not exist')
327 self.drv.delete_share(self.ctx, share)
329 @mock.patch(PATH_TO_RPC)
330 def test_delete_share__some_error(self, post):
331 share = {
332 'name': 'share',
333 'size': 1,
334 'share_proto': self.cfg.enabled_share_protocols
335 }
336 post.return_value = FakeResponse()
337 post.side_effect = exception.ManilaException('Some error')
339 self.assertRaises(
340 exception.ManilaException, self.drv.delete_share, self.ctx, share)
342 @mock.patch(PATH_TO_RPC)
343 def test_extend_share__thin_provisoning(self, post):
344 share = {
345 'name': 'share',
346 'size': 1,
347 'share_proto': self.cfg.enabled_share_protocols
348 }
349 new_size = 5
350 quota = '%sG' % new_size
351 post.return_value = FakeResponse()
352 self.cfg.nexenta_thin_provisioning = True
354 self.drv.extend_share(share, new_size)
356 post.assert_called_with(
357 self.request_params.url,
358 data=self.request_params.build_post_args(
359 'folder',
360 'set_child_prop',
361 '%s/%s/%s' % (self.volume, self.share, share['name']),
362 'quota', quota),
363 headers=self.request_params.headers,
364 timeout=60)
366 @mock.patch(PATH_TO_RPC)
367 def test_extend_share__thick_provisoning(self, post):
368 share = {
369 'name': 'share',
370 'size': 1,
371 'share_proto': self.cfg.enabled_share_protocols
372 }
373 new_size = 5
374 post.return_value = FakeResponse()
375 self.cfg.nexenta_thin_provisioning = False
377 self.drv.extend_share(share, new_size)
379 post.assert_not_called()
381 @mock.patch(PATH_TO_RPC)
382 def test_create_snapshot(self, post):
383 snapshot = {'share_name': 'share', 'name': 'share@first'}
384 post.return_value = FakeResponse()
385 folder = '%s/%s/%s' % (self.volume, self.share, snapshot['share_name'])
387 self.drv.create_snapshot(self.ctx, snapshot)
389 post.assert_called_with(
390 self.request_params.url, data=self.request_params.build_post_args(
391 'folder', 'create_snapshot', folder, snapshot['name'], '-r'),
392 headers=self.request_params.headers, timeout=60)
394 @mock.patch(PATH_TO_RPC)
395 def test_delete_snapshot(self, post):
396 snapshot = {'share_name': 'share', 'name': 'share@first'}
397 post.return_value = FakeResponse()
399 self.drv.delete_snapshot(self.ctx, snapshot)
401 post.assert_called_with(
402 self.request_params.url, data=self.request_params.build_post_args(
403 'snapshot', 'destroy', '%s@%s' % (
404 self._get_share_path(snapshot['share_name']),
405 snapshot['name']),
406 ''),
407 headers=self.request_params.headers,
408 timeout=60)
410 @mock.patch(PATH_TO_RPC)
411 def test_delete_snapshot__nexenta_error_1(self, post):
412 snapshot = {'share_name': 'share', 'name': 'share@first'}
413 post.return_value = FakeResponse()
414 post.side_effect = exception.NexentaException('does not exist')
416 self.drv.delete_snapshot(self.ctx, snapshot)
418 @mock.patch(PATH_TO_RPC)
419 def test_delete_snapshot__nexenta_error_2(self, post):
420 snapshot = {'share_name': 'share', 'name': 'share@first'}
421 post.return_value = FakeResponse()
422 post.side_effect = exception.NexentaException('has dependent clones')
424 self.drv.delete_snapshot(self.ctx, snapshot)
426 @mock.patch(PATH_TO_RPC)
427 def test_delete_snapshot__some_error(self, post):
428 snapshot = {'share_name': 'share', 'name': 'share@first'}
429 post.return_value = FakeResponse()
430 post.side_effect = exception.ManilaException('Some error')
432 self.assertRaises(exception.ManilaException, self.drv.delete_snapshot,
433 self.ctx, snapshot)
435 @mock.patch(PATH_TO_RPC)
436 def test_update_access__unsupported_access_type(self, post):
437 share = {
438 'name': 'share',
439 'share_proto': self.cfg.enabled_share_protocols
440 }
441 access = {
442 'access_type': 'group',
443 'access_to': 'ordinary_users',
444 'access_level': 'rw'
445 }
447 self.assertRaises(exception.InvalidShareAccess,
448 self.drv.update_access,
449 self.ctx,
450 share,
451 [access],
452 None,
453 None,
454 None)
456 @mock.patch(PATH_TO_RPC)
457 def test_update_access__cidr(self, post):
458 share = {
459 'name': 'share',
460 'share_proto': self.cfg.enabled_share_protocols
461 }
462 access1 = {
463 'access_type': 'ip',
464 'access_to': '1.1.1.1/24',
465 'access_level': 'rw'
466 }
467 access2 = {
468 'access_type': 'ip',
469 'access_to': '1.2.3.4',
470 'access_level': 'rw'
471 }
472 access_rules = [access1, access2]
474 share_opts = {
475 'auth_type': 'none',
476 'read_write': '%s:%s' % (
477 access1['access_to'], access2['access_to']),
478 'read_only': '',
479 'recursive': 'true',
480 'anonymous_rw': 'true',
481 'anonymous': 'true',
482 'extra_options': 'anon=0',
483 }
485 def my_side_effect(*args, **kwargs):
486 if kwargs['data'] == self.request_params.build_post_args(
487 'netstorsvc', 'share_folder',
488 'svc:/network/nfs/server:default',
489 self._get_share_path(share['name']), share_opts):
490 return FakeResponse()
491 else:
492 raise exception.ManilaException('Unexpected request')
494 post.return_value = FakeResponse()
495 post.side_effect = my_side_effect
497 self.drv.update_access(self.ctx, share, access_rules, None, None, None)
499 post.assert_called_with(
500 self.request_params.url, data=self.request_params.build_post_args(
501 'netstorsvc', 'share_folder',
502 'svc:/network/nfs/server:default',
503 self._get_share_path(share['name']), share_opts),
504 headers=self.request_params.headers,
505 timeout=60)
506 self.assertRaises(exception.ManilaException, self.drv.update_access,
507 self.ctx, share,
508 [access1, {'access_type': 'ip',
509 'access_to': '2.2.2.2',
510 'access_level': 'rw'}],
511 None, None, None)
513 @mock.patch(PATH_TO_RPC)
514 def test_update_access__add_one_ip_to_empty_access_list(self, post):
515 share = {'name': 'share',
516 'share_proto': self.cfg.enabled_share_protocols}
517 access = {
518 'access_type': 'ip',
519 'access_to': '1.1.1.1',
520 'access_level': 'rw'
521 }
523 rw_list = None
524 share_opts = {
525 'auth_type': 'none',
526 'read_write': access['access_to'],
527 'read_only': '',
528 'recursive': 'true',
529 'anonymous_rw': 'true',
530 'anonymous': 'true',
531 'extra_options': 'anon=0',
532 }
534 def my_side_effect(*args, **kwargs):
535 if kwargs['data'] == self.request_params.build_post_args( 535 ↛ 539line 535 didn't jump to line 539 because the condition on line 535 was never true
536 'netstorsvc', 'get_shareopts',
537 'svc:/network/nfs/server:default',
538 self._get_share_path(share['name'])):
539 return FakeResponse({'result': {'read_write': rw_list}})
540 elif kwargs['data'] == self.request_params.build_post_args( 540 ↛ 544line 540 didn't jump to line 544 because the condition on line 540 was never true
541 'netstorsvc', 'share_folder',
542 'svc:/network/nfs/server:default',
543 self._get_share_path(share['name']), share_opts):
544 return FakeResponse()
545 else:
546 raise exception.ManilaException('Unexpected request')
547 post.return_value = FakeResponse()
549 self.drv.update_access(self.ctx, share, [access], None, None, None)
551 post.assert_called_with(
552 self.request_params.url, data=self.request_params.build_post_args(
553 'netstorsvc', 'share_folder',
554 'svc:/network/nfs/server:default',
555 self._get_share_path(share['name']), share_opts),
556 headers=self.request_params.headers,
557 timeout=60)
559 post.side_effect = my_side_effect
561 self.assertRaises(exception.ManilaException, self.drv.update_access,
562 self.ctx, share,
563 [{'access_type': 'ip',
564 'access_to': '1111',
565 'access_level': 'rw'}],
566 None, None, None)
568 @mock.patch(PATH_TO_RPC)
569 def test_deny_access__unsupported_access_type(self, post):
570 share = {'name': 'share',
571 'share_proto': self.cfg.enabled_share_protocols}
572 access = {
573 'access_type': 'group',
574 'access_to': 'ordinary_users',
575 'access_level': 'rw'
576 }
578 self.assertRaises(exception.InvalidShareAccess, self.drv.update_access,
579 self.ctx, share, [access], None, None, None)
581 def test_share_backend_name(self):
582 self.assertEqual('NexentaStor', self.drv.share_backend_name)
584 @mock.patch(PATH_TO_RPC)
585 def test_get_capacity_info(self, post):
586 post.return_value = FakeResponse({'result': {
587 'available': 9 * units.Gi, 'used': 1 * units.Gi}})
589 self.assertEqual(
590 (10, 9, 1), self.drv.helper._get_capacity_info())
592 @mock.patch('manila.share.drivers.nexenta.ns4.nexenta_nfs_helper.'
593 'NFSHelper._get_capacity_info')
594 @mock.patch('manila.share.driver.ShareDriver._update_share_stats')
595 def test_update_share_stats(self, super_stats, info):
596 info.return_value = (100, 90, 10)
597 stats = {
598 'vendor_name': 'Nexenta',
599 'storage_protocol': 'NFS',
600 'nfs_mount_point_base': self.cfg.nexenta_mount_point_base,
601 'driver_version': '1.0',
602 'share_backend_name': self.cfg.share_backend_name,
603 'pools': [{
604 'total_capacity_gb': 100,
605 'free_capacity_gb': 90,
606 'pool_name': 'volume',
607 'reserved_percentage': (
608 self.cfg.reserved_share_percentage),
609 'reserved_snapshot_percentage': (
610 self.cfg.reserved_share_from_snapshot_percentage),
611 'reserved_share_extend_percentage': (
612 self.cfg.reserved_share_extend_percentage),
613 'compression': True,
614 'dedupe': True,
615 'thin_provisioning': self.cfg.nexenta_thin_provisioning,
616 'max_over_subscription_ratio': (
617 self.cfg.safe_get(
618 'max_over_subscription_ratio')),
619 }],
620 }
622 self.drv._update_share_stats()
624 self.assertEqual(stats, self.drv._stats)