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

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. 

15 

16import base64 

17import json 

18from unittest import mock 

19 

20from oslo_serialization import jsonutils 

21from oslo_utils import units 

22 

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 

28 

29PATH_TO_RPC = 'requests.post' 

30CODE = mock.PropertyMock(return_value=200) 

31 

32 

33class FakeResponse(object): 

34 

35 def __init__(self, response={}): 

36 self.content = json.dumps(response) 

37 super(FakeResponse, self).__init__() 

38 

39 def close(self): 

40 pass 

41 

42 

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 

51 

52 @property 

53 def url(self): 

54 return '%s://%s:%s%s' % (self.scheme, self.host, self.port, self.path) 

55 

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 

65 

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 

73 

74 

75class TestNexentaNasDriver(test.TestCase): 

76 

77 def _get_share_path(self, share_name): 

78 return '%s/%s/%s' % (self.volume, self.share, share_name) 

79 

80 def setUp(self): 

81 def _safe_get(opt): 

82 return getattr(self.cfg, opt) 

83 

84 self.cfg = mock.Mock(spec=conf.Configuration) 

85 self.cfg.nexenta_nas_host = '1.1.1.1' 

86 super(TestNexentaNasDriver, self).setUp() 

87 

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' 

108 

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 

113 

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) 

117 

118 self.drv = nexenta_nas.NexentaNasDriver(configuration=self.cfg) 

119 self.drv.do_setup(self.ctx) 

120 

121 self.volume = self.cfg.nexenta_volume 

122 self.share = self.cfg.nexenta_nfs_share 

123 

124 @mock.patch(PATH_TO_RPC) 

125 def test_check_for_setup_error__volume_doesnt_exist(self, post): 

126 post.return_value = FakeResponse() 

127 

128 self.assertRaises( 

129 exception.NexentaException, self.drv.check_for_setup_error) 

130 

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 } 

141 

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 } 

150 

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 

169 

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) 

182 

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() 

195 

196 self.assertEqual([location], 

197 self.drv.create_share(self.ctx, share)) 

198 

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() 

207 

208 self.assertRaises(exception.InvalidShare, self.drv.create_share, 

209 self.ctx, share) 

210 

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 

223 

224 self.drv.create_share(self.ctx, share) 

225 

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) 

236 

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 

254 

255 self.drv.create_share(self.ctx, share) 

256 

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) 

267 

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']) 

282 

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) 

294 

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']) 

304 

305 self.drv.delete_share(self.ctx, share) 

306 

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) 

316 

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') 

326 

327 self.drv.delete_share(self.ctx, share) 

328 

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') 

338 

339 self.assertRaises( 

340 exception.ManilaException, self.drv.delete_share, self.ctx, share) 

341 

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 

353 

354 self.drv.extend_share(share, new_size) 

355 

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) 

365 

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 

376 

377 self.drv.extend_share(share, new_size) 

378 

379 post.assert_not_called() 

380 

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']) 

386 

387 self.drv.create_snapshot(self.ctx, snapshot) 

388 

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) 

393 

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() 

398 

399 self.drv.delete_snapshot(self.ctx, snapshot) 

400 

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) 

409 

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') 

415 

416 self.drv.delete_snapshot(self.ctx, snapshot) 

417 

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') 

423 

424 self.drv.delete_snapshot(self.ctx, snapshot) 

425 

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') 

431 

432 self.assertRaises(exception.ManilaException, self.drv.delete_snapshot, 

433 self.ctx, snapshot) 

434 

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 } 

446 

447 self.assertRaises(exception.InvalidShareAccess, 

448 self.drv.update_access, 

449 self.ctx, 

450 share, 

451 [access], 

452 None, 

453 None, 

454 None) 

455 

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] 

473 

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 } 

484 

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') 

493 

494 post.return_value = FakeResponse() 

495 post.side_effect = my_side_effect 

496 

497 self.drv.update_access(self.ctx, share, access_rules, None, None, None) 

498 

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) 

512 

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 } 

522 

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 } 

533 

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() 

548 

549 self.drv.update_access(self.ctx, share, [access], None, None, None) 

550 

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) 

558 

559 post.side_effect = my_side_effect 

560 

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) 

567 

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 } 

577 

578 self.assertRaises(exception.InvalidShareAccess, self.drv.update_access, 

579 self.ctx, share, [access], None, None, None) 

580 

581 def test_share_backend_name(self): 

582 self.assertEqual('NexentaStor', self.drv.share_backend_name) 

583 

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}}) 

588 

589 self.assertEqual( 

590 (10, 9, 1), self.drv.helper._get_capacity_info()) 

591 

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 } 

621 

622 self.drv._update_share_stats() 

623 

624 self.assertEqual(stats, self.drv._stats)