Coverage for manila/tests/share/drivers/netapp/dataontap/client/test_api.py: 99%

312 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright (c) 2014 Ben Swartzlander. All rights reserved. 

2# Copyright (c) 2014 Navneet Singh. All rights reserved. 

3# Copyright (c) 2014 Clinton Knight. All rights reserved. 

4# Copyright (c) 2014 Alex Meade. All rights reserved. 

5# Copyright (c) 2014 Bob Callaway. All rights reserved. 

6# 

7# Licensed under the Apache License, Version 2.0 (the "License"); you may 

8# not use this file except in compliance with the License. You may obtain 

9# a copy of the License at 

10# 

11# http://www.apache.org/licenses/LICENSE-2.0 

12# 

13# Unless required by applicable law or agreed to in writing, software 

14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

16# License for the specific language governing permissions and limitations 

17# under the License. 

18""" 

19Tests for NetApp API layer 

20""" 

21 

22from oslo_serialization import jsonutils 

23from unittest import mock 

24 

25import ddt 

26import requests 

27 

28from manila import exception 

29from manila.share.drivers.netapp.dataontap.client import api 

30from manila.share.drivers.netapp.dataontap.client import rest_endpoints 

31from manila import test 

32from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake 

33 

34 

35class NetAppApiElementTransTests(test.TestCase): 

36 """Test case for NetApp API element translations.""" 

37 

38 def test_get_set_system_version(self): 

39 napi = api.NaServer('localhost') 

40 

41 # Testing calls before version is set 

42 version = napi.get_system_version() 

43 self.assertIsNone(version) 

44 napi.set_system_version(fake.VERSION_TUPLE) 

45 version = napi.get_system_version() 

46 self.assertEqual(fake.VERSION_TUPLE, version) 

47 

48 def test_translate_struct_dict_unique_key(self): 

49 """Tests if dict gets properly converted to NaElements.""" 

50 root = api.NaElement('root') 

51 child = {'e1': 'v1', 'e2': 'v2', 'e3': 'v3'} 

52 

53 root.translate_struct(child) 

54 

55 self.assertEqual(3, len(root.get_children())) 

56 for key, value in child.items(): 

57 self.assertEqual(value, root.get_child_content(key)) 

58 

59 def test_translate_struct_dict_nonunique_key(self): 

60 """Tests if list/dict gets properly converted to NaElements.""" 

61 root = api.NaElement('root') 

62 child = [{'e1': 'v1', 'e2': 'v2'}, {'e1': 'v3'}] 

63 

64 root.translate_struct(child) 

65 

66 children = root.get_children() 

67 self.assertEqual(3, len(children)) 

68 for c in children: 

69 if c.get_name() == 'e1': 

70 self.assertIn(c.get_content(), ['v1', 'v3']) 

71 else: 

72 self.assertEqual('v2', c.get_content()) 

73 

74 def test_translate_struct_list(self): 

75 """Tests if list gets properly converted to NaElements.""" 

76 root = api.NaElement('root') 

77 child = ['e1', 'e2'] 

78 

79 root.translate_struct(child) 

80 

81 self.assertEqual(2, len(root.get_children())) 

82 self.assertIsNone(root.get_child_content('e1')) 

83 self.assertIsNone(root.get_child_content('e2')) 

84 

85 def test_translate_struct_tuple(self): 

86 """Tests if tuple gets properly converted to NaElements.""" 

87 root = api.NaElement('root') 

88 child = ('e1', 'e2') 

89 

90 root.translate_struct(child) 

91 

92 self.assertEqual(2, len(root.get_children())) 

93 self.assertIsNone(root.get_child_content('e1')) 

94 self.assertIsNone(root.get_child_content('e2')) 

95 

96 def test_translate_invalid_struct(self): 

97 """Tests if invalid data structure raises exception.""" 

98 root = api.NaElement('root') 

99 child = 'random child element' 

100 self.assertRaises(ValueError, root.translate_struct, child) 

101 

102 def test_setter_builtin_types(self): 

103 """Tests str, int, float get converted to NaElement.""" 

104 update = dict(e1='v1', e2='1', e3='2.0', e4='8') 

105 root = api.NaElement('root') 

106 

107 for key, value in update.items(): 

108 root[key] = value 

109 

110 for key, value in update.items(): 

111 self.assertEqual(value, root.get_child_content(key)) 

112 

113 def test_setter_na_element(self): 

114 """Tests na_element gets appended as child.""" 

115 root = api.NaElement('root') 

116 root['e1'] = api.NaElement('nested') 

117 self.assertEqual(1, len(root.get_children())) 

118 e1 = root.get_child_by_name('e1') 

119 self.assertIsInstance(e1, api.NaElement) 

120 self.assertIsInstance(e1.get_child_by_name('nested'), api.NaElement) 

121 

122 def test_setter_child_dict(self): 

123 """Tests dict is appended as child to root.""" 

124 root = api.NaElement('root') 

125 root['d'] = {'e1': 'v1', 'e2': 'v2'} 

126 e1 = root.get_child_by_name('d') 

127 self.assertIsInstance(e1, api.NaElement) 

128 sub_ch = e1.get_children() 

129 self.assertEqual(2, len(sub_ch)) 

130 for c in sub_ch: 

131 self.assertIn(c.get_name(), ['e1', 'e2']) 

132 if c.get_name() == 'e1': 

133 self.assertEqual('v1', c.get_content()) 

134 else: 

135 self.assertEqual('v2', c.get_content()) 

136 

137 def test_setter_child_list_tuple(self): 

138 """Tests list/tuple are appended as child to root.""" 

139 root = api.NaElement('root') 

140 

141 root['l'] = ['l1', 'l2'] 

142 root['t'] = ('t1', 't2') 

143 

144 li = root.get_child_by_name('l') 

145 self.assertIsInstance(li, api.NaElement) 

146 t = root.get_child_by_name('t') 

147 self.assertIsInstance(t, api.NaElement) 

148 

149 self.assertEqual(2, len(li.get_children())) 

150 for le in li.get_children(): 

151 self.assertIn(le.get_name(), ['l1', 'l2']) 

152 

153 self.assertEqual(2, len(t.get_children())) 

154 for te in t.get_children(): 

155 self.assertIn(te.get_name(), ['t1', 't2']) 

156 

157 def test_setter_no_value(self): 

158 """Tests key with None value.""" 

159 root = api.NaElement('root') 

160 root['k'] = None 

161 self.assertIsNone(root.get_child_content('k')) 

162 

163 def test_setter_invalid_value(self): 

164 """Tests invalid value raises exception.""" 

165 self.assertRaises(TypeError, 

166 api.NaElement('root').__setitem__, 

167 'k', 

168 api.NaServer('localhost')) 

169 

170 def test_setter_invalid_key(self): 

171 """Tests invalid value raises exception.""" 

172 self.assertRaises(KeyError, 

173 api.NaElement('root').__setitem__, 

174 None, 

175 'value') 

176 

177 def test__build_session_with_basic_auth(self): 

178 """Tests whether build session works with """ 

179 """default(basic auth) parameters""" 

180 napi = api.ZapiClient('localhost') 

181 fake_session = mock.Mock() 

182 mock_requests_session = self.mock_object( 

183 requests, 'Session', mock.Mock(return_value=fake_session)) 

184 mock_auth = self.mock_object(napi, '_create_basic_auth_handler', 

185 mock.Mock(return_value='fake_auth')) 

186 napi._ssl_verify = 'fake_ssl' 

187 fake_headers = {'Content-Type': 'text/xml'} 

188 napi._build_session() 

189 

190 self.assertEqual(fake_session, napi._session) 

191 self.assertEqual('fake_auth', napi._session.auth) 

192 self.assertEqual('fake_ssl', napi._session.verify) 

193 self.assertEqual(fake_headers, 

194 napi._session.headers) 

195 mock_requests_session.assert_called_once_with() 

196 mock_auth.assert_called_once_with() 

197 

198 def test__build_session_with_certificate_auth(self): 

199 """Tests whether build session works with """ 

200 """valid certificate parameters""" 

201 napi = api.ZapiClient('localhost') 

202 napi._private_key_file = 'fake_key.pem' 

203 napi._certificate_file = 'fake_cert.pem' 

204 napi._certificate_host_validation = False 

205 cert = napi._certificate_file, napi._private_key_file 

206 fake_headers = {'Content-Type': 'text/xml'} 

207 fake_session = mock.Mock() 

208 napi._session = mock.Mock() 

209 mock_requests_session = self.mock_object( 

210 requests, 'Session', mock.Mock(return_value=fake_session)) 

211 res = napi._create_certificate_auth_handler() 

212 napi._build_session() 

213 self.assertEqual(fake_session, napi._session) 

214 self.assertEqual(res, 

215 (cert, napi._certificate_host_validation)) 

216 self.assertEqual(fake_headers, 

217 napi._session.headers) 

218 mock_requests_session.assert_called_once_with() 

219 

220 def test__create_certificate_auth_handler_default(self): 

221 """Test whether create certificate auth handler """ 

222 """works with default params""" 

223 napi = api.ZapiClient('localhost') 

224 napi._private_key_file = 'fake_key.pem' 

225 napi._certificate_file = 'fake_cert.pem' 

226 napi._certificate_host_validation = False 

227 cert = napi._certificate_file, napi._private_key_file 

228 napi._session = mock.Mock() 

229 if not napi._certificate_host_validation: 229 ↛ 231line 229 didn't jump to line 231 because the condition on line 229 was always true

230 self.assertFalse(napi._certificate_host_validation) 

231 res = napi._create_certificate_auth_handler() 

232 self.assertEqual(res, (cert, napi._certificate_host_validation)) 

233 

234 def test__create_certificate_auth_handler_with_host_validation(self): 

235 """Test whether create certificate auth handler """ 

236 """works with host validation enabled""" 

237 napi = api.ZapiClient('localhost') 

238 napi._private_key_file = 'fake_key.pem' 

239 napi._certificate_file = 'fake_cert.pem' 

240 napi._ca_certificate_file = 'fake_ca_cert.crt' 

241 napi._certificate_host_validation = True 

242 cert = napi._certificate_file, napi._private_key_file 

243 napi._session = mock.Mock() 

244 if napi._certificate_host_validation: 244 ↛ 246line 244 didn't jump to line 246 because the condition on line 244 was always true

245 self.assertTrue(napi._certificate_host_validation) 

246 res = napi._create_certificate_auth_handler() 

247 self.assertEqual(res, (cert, napi._ca_certificate_file)) 

248 

249 

250@ddt.ddt 

251class NetAppApiServerZapiClientTests(test.TestCase): 

252 """Test case for NetApp API server methods""" 

253 def setUp(self): 

254 self.root = api.NaServer('127.0.0.1').zapi_client 

255 super(NetAppApiServerZapiClientTests, self).setUp() 

256 

257 @ddt.data(None, fake.FAKE_XML_STR) 

258 def test_invoke_elem_value_error(self, na_element): 

259 """Tests whether invalid NaElement parameter causes error""" 

260 

261 self.assertRaises(ValueError, self.root.invoke_elem, na_element) 

262 

263 def test_invoke_elem_http_error(self): 

264 """Tests handling of HTTPError""" 

265 na_element = fake.FAKE_NA_ELEMENT 

266 self.mock_object(self.root, '_create_request', mock.Mock( 

267 return_value=fake.FAKE_NA_ELEMENT)) 

268 self.mock_object(api, 'LOG') 

269 self.root._session = fake.FAKE_HTTP_SESSION 

270 self.mock_object(self.root, '_build_session') 

271 self.mock_object(self.root._session, 'post', mock.Mock( 

272 side_effect=requests.HTTPError())) 

273 

274 self.assertRaises(api.NaApiError, self.root.invoke_elem, 

275 na_element) 

276 

277 def test_invoke_elem_urlerror(self): 

278 """Tests handling of URLError""" 

279 na_element = fake.FAKE_NA_ELEMENT 

280 self.mock_object(self.root, '_create_request', mock.Mock( 

281 return_value=fake.FAKE_NA_ELEMENT)) 

282 self.mock_object(api, 'LOG') 

283 self.root._session = fake.FAKE_HTTP_SESSION 

284 self.mock_object(self.root, '_build_session') 

285 self.mock_object(self.root._session, 'post', mock.Mock( 

286 side_effect=requests.URLRequired())) 

287 

288 self.assertRaises(exception.StorageCommunicationException, 

289 self.root.invoke_elem, 

290 na_element) 

291 

292 def test_invoke_elem_unknown_exception(self): 

293 """Tests handling of Unknown Exception""" 

294 na_element = fake.FAKE_NA_ELEMENT 

295 self.mock_object(self.root, '_create_request', mock.Mock( 

296 return_value=fake.FAKE_NA_ELEMENT)) 

297 self.mock_object(api, 'LOG') 

298 self.root._session = fake.FAKE_HTTP_SESSION 

299 self.mock_object(self.root, '_build_session') 

300 self.mock_object(self.root._session, 'post', mock.Mock( 

301 side_effect=Exception)) 

302 

303 exception = self.assertRaises(api.NaApiError, self.root.invoke_elem, 

304 na_element) 

305 self.assertEqual('unknown', exception.code) 

306 

307 @ddt.data({'trace_enabled': False, 

308 'trace_pattern': '(.*)', 'log': False}, 

309 {'trace_enabled': True, 

310 'trace_pattern': '(?!(volume)).*', 'log': False}, 

311 {'trace_enabled': True, 

312 'trace_pattern': '(.*)', 'log': True}, 

313 {'trace_enabled': True, 

314 'trace_pattern': '^volume-(info|get-iter)$', 'log': True}) 

315 @ddt.unpack 

316 def test_invoke_elem_valid(self, trace_enabled, trace_pattern, log): 

317 """Tests the method invoke_elem with valid parameters""" 

318 na_element = fake.FAKE_NA_ELEMENT 

319 self.root._trace = trace_enabled 

320 self.root._api_trace_pattern = trace_pattern 

321 self.mock_object(self.root, '_create_request', mock.Mock( 

322 return_value=fake.FAKE_NA_ELEMENT)) 

323 self.mock_object(api, 'LOG') 

324 self.root._session = fake.FAKE_HTTP_SESSION 

325 self.mock_object(self.root, '_build_session') 

326 self.mock_object(self.root, '_get_result', mock.Mock( 

327 return_value=fake.FAKE_NA_ELEMENT)) 

328 

329 response = mock.Mock() 

330 response.text = 'res1' 

331 self.mock_object( 

332 self.root._session, 'post', mock.Mock( 

333 return_value=response)) 

334 

335 self.root.invoke_elem(na_element) 

336 

337 expected_log_count = 2 if log else 0 

338 self.assertEqual(expected_log_count, api.LOG.debug.call_count) 

339 

340 @ddt.data('1234', 5678) 

341 def test_custom_port(self, port): 

342 root = api.NaServer('127.0.0.1', port=port).zapi_client 

343 self.assertEqual(str(port), root.get_port()) 

344 

345 

346@ddt.ddt 

347class NetAppApiServerRestClientTests(test.TestCase): 

348 """Test case for NetApp API Rest server methods""" 

349 def setUp(self): 

350 self.root = api.NaServer('127.0.0.1').rest_client 

351 super(NetAppApiServerRestClientTests, self).setUp() 

352 

353 def test_invoke_elem_value_error(self): 

354 """Tests whether invalid NaElement parameter causes error""" 

355 na_element = fake.FAKE_REST_CALL_STR 

356 self.assertRaises(ValueError, self.root.invoke_elem, na_element) 

357 

358 def _setup_mocks_for_invoke_element(self, mock_post_action): 

359 

360 self.mock_object(api, 'LOG') 

361 self.root._session = fake.FAKE_HTTP_SESSION 

362 self.root._session.post = mock_post_action 

363 self.mock_object(self.root, '_build_session') 

364 self.mock_object( 

365 self.root, '_get_request_info', mock.Mock( 

366 return_value=(self.root._session.post, fake.FAKE_ACTION_URL))) 

367 self.mock_object( 

368 self.root, '_get_base_url', 

369 mock.Mock(return_value=fake.FAKE_BASE_URL)) 

370 

371 return fake.FAKE_BASE_URL 

372 

373 def test_invoke_elem_http_error(self): 

374 """Tests handling of HTTPError""" 

375 na_element = fake.FAKE_NA_ELEMENT 

376 element_name = fake.FAKE_NA_ELEMENT.get_name() 

377 self._setup_mocks_for_invoke_element( 

378 mock_post_action=mock.Mock(side_effect=requests.HTTPError())) 

379 

380 self.assertRaises(api.NaApiError, self.root.invoke_elem, 

381 na_element) 

382 self.assertTrue(self.root._get_base_url.called) 

383 self.root._get_request_info.assert_called_once_with( 

384 element_name, self.root._session) 

385 

386 def test_invoke_elem_urlerror(self): 

387 """Tests handling of URLError""" 

388 na_element = fake.FAKE_NA_ELEMENT 

389 element_name = fake.FAKE_NA_ELEMENT.get_name() 

390 self._setup_mocks_for_invoke_element( 

391 mock_post_action=mock.Mock(side_effect=requests.URLRequired())) 

392 

393 self.assertRaises(exception.StorageCommunicationException, 

394 self.root.invoke_elem, 

395 na_element) 

396 

397 self.assertTrue(self.root._get_base_url.called) 

398 self.root._get_request_info.assert_called_once_with( 

399 element_name, self.root._session) 

400 

401 def test_invoke_elem_unknown_exception(self): 

402 """Tests handling of Unknown Exception""" 

403 na_element = fake.FAKE_NA_ELEMENT 

404 element_name = fake.FAKE_NA_ELEMENT.get_name() 

405 self._setup_mocks_for_invoke_element( 

406 mock_post_action=mock.Mock(side_effect=Exception)) 

407 

408 exception = self.assertRaises(api.NaApiError, self.root.invoke_elem, 

409 na_element) 

410 self.assertEqual('unknown', exception.code) 

411 self.assertTrue(self.root._get_base_url.called) 

412 self.root._get_request_info.assert_called_once_with( 

413 element_name, self.root._session) 

414 

415 @ddt.data( 

416 {'trace_enabled': False, 

417 'trace_pattern': '(.*)', 

418 'log': False, 

419 'query': None, 

420 'body': fake.FAKE_HTTP_BODY 

421 }, 

422 {'trace_enabled': True, 

423 'trace_pattern': '(?!(volume)).*', 

424 'log': False, 

425 'query': None, 

426 'body': fake.FAKE_HTTP_BODY 

427 }, 

428 {'trace_enabled': True, 

429 'trace_pattern': '(.*)', 

430 'log': True, 

431 'query': fake.FAKE_HTTP_QUERY, 

432 'body': fake.FAKE_HTTP_BODY 

433 }, 

434 {'trace_enabled': True, 

435 'trace_pattern': '^volume-(info|get-iter)$', 

436 'log': True, 

437 'query': fake.FAKE_HTTP_QUERY, 

438 'body': fake.FAKE_HTTP_BODY 

439 } 

440 ) 

441 @ddt.unpack 

442 def test_invoke_elem_valid(self, trace_enabled, trace_pattern, log, query, 

443 body): 

444 """Tests the method invoke_elem with valid parameters""" 

445 self.root._session = fake.FAKE_HTTP_SESSION 

446 response = mock.Mock() 

447 response.content = 'fake_response' 

448 self.root._session.post = mock.Mock(return_value=response) 

449 na_element = fake.FAKE_NA_ELEMENT 

450 element_name = fake.FAKE_NA_ELEMENT.get_name() 

451 self.root._trace = trace_enabled 

452 self.root._api_trace_pattern = trace_pattern 

453 expected_url = fake.FAKE_BASE_URL + fake.FAKE_ACTION_URL 

454 

455 api_args = { 

456 "body": body, 

457 "query": query 

458 } 

459 

460 self.mock_object(api, 'LOG') 

461 mock_build_session = self.mock_object(self.root, '_build_session') 

462 mock_get_req_info = self.mock_object( 

463 self.root, '_get_request_info', mock.Mock( 

464 return_value=(self.root._session.post, fake.FAKE_ACTION_URL))) 

465 mock_add_query_params = self.mock_object( 

466 self.root, '_add_query_params_to_url', mock.Mock( 

467 return_value=fake.FAKE_ACTION_URL)) 

468 mock_get_base_url = self.mock_object( 

469 self.root, '_get_base_url', 

470 mock.Mock(return_value=fake.FAKE_BASE_URL)) 

471 mock_json_loads = self.mock_object( 

472 jsonutils, 'loads', mock.Mock(return_value='fake_response')) 

473 mock_json_dumps = self.mock_object( 

474 jsonutils, 'dumps', mock.Mock(return_value=body)) 

475 

476 result = self.root.invoke_elem(na_element, api_args=api_args) 

477 

478 self.assertEqual('fake_response', result) 

479 expected_log_count = 2 if log else 0 

480 self.assertEqual(expected_log_count, api.LOG.debug.call_count) 

481 self.assertTrue(mock_build_session.called) 

482 mock_get_req_info.assert_called_once_with( 

483 element_name, self.root._session) 

484 if query: 

485 mock_add_query_params.assert_called_once_with( 

486 fake.FAKE_ACTION_URL, query) 

487 self.assertTrue(mock_get_base_url.called) 

488 self.root._session.post.assert_called_once_with( 

489 expected_url, data=body) 

490 mock_json_loads.assert_called_once_with('fake_response') 

491 mock_json_dumps.assert_called_once_with(body) 

492 

493 @ddt.data( 

494 ('svm-migration-start', rest_endpoints.ENDPOINT_MIGRATIONS, 'post'), 

495 ('svm-migration-complete', rest_endpoints.ENDPOINT_MIGRATION_ACTIONS, 

496 'patch') 

497 ) 

498 @ddt.unpack 

499 def test__get_request_info(self, api_name, expected_url, expected_method): 

500 self.root._session = fake.FAKE_HTTP_SESSION 

501 for http_method in ['post', 'get', 'put', 'delete', 'patch']: 

502 setattr(self.root._session, http_method, mock.Mock()) 

503 

504 method, url = self.root._get_request_info(api_name, self.root._session) 

505 

506 self.assertEqual(method, getattr(self.root._session, expected_method)) 

507 self.assertEqual(expected_url, url) 

508 

509 @ddt.data( 

510 {'is_ipv6': False, 'protocol': 'http', 'port': '80'}, 

511 {'is_ipv6': False, 'protocol': 'https', 'port': '443'}, 

512 {'is_ipv6': True, 'protocol': 'http', 'port': '80'}, 

513 {'is_ipv6': True, 'protocol': 'https', 'port': '443'}) 

514 @ddt.unpack 

515 def test__get_base_url(self, is_ipv6, protocol, port): 

516 self.root._host = '10.0.0.3' if not is_ipv6 else 'FF01::1' 

517 self.root._protocol = protocol 

518 self.root._port = port 

519 

520 host_formated_for_url = ( 

521 '[%s]' % self.root._host if is_ipv6 else self.root._host) 

522 

523 # example of the expected format: http://10.0.0.3:80/api/ 

524 expected_result = ( 

525 protocol + '://' + host_formated_for_url + ':' + port + '/api/') 

526 

527 base_url = self.root._get_base_url() 

528 

529 self.assertEqual(expected_result, base_url) 

530 

531 def test__add_query_params_to_url(self): 

532 url = 'endpoint/to/get/data' 

533 filters = "?" 

534 for k, v in fake.FAKE_HTTP_QUERY.items(): 

535 filters += "%(key)s=%(value)s&" % {"key": k, "value": v} 

536 expected_formated_url = url + filters 

537 

538 formatted_url = self.root._add_query_params_to_url( 

539 url, fake.FAKE_HTTP_QUERY) 

540 

541 self.assertEqual(expected_formated_url, formatted_url) 

542 

543 @ddt.data('1234', 5678) 

544 def test_custom_port(self, port): 

545 root = api.NaServer('127.0.0.1', port=port).rest_client 

546 self.assertEqual(str(port), root.get_port())