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
« 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"""
22from oslo_serialization import jsonutils
23from unittest import mock
25import ddt
26import requests
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
35class NetAppApiElementTransTests(test.TestCase):
36 """Test case for NetApp API element translations."""
38 def test_get_set_system_version(self):
39 napi = api.NaServer('localhost')
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)
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'}
53 root.translate_struct(child)
55 self.assertEqual(3, len(root.get_children()))
56 for key, value in child.items():
57 self.assertEqual(value, root.get_child_content(key))
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'}]
64 root.translate_struct(child)
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())
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']
79 root.translate_struct(child)
81 self.assertEqual(2, len(root.get_children()))
82 self.assertIsNone(root.get_child_content('e1'))
83 self.assertIsNone(root.get_child_content('e2'))
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')
90 root.translate_struct(child)
92 self.assertEqual(2, len(root.get_children()))
93 self.assertIsNone(root.get_child_content('e1'))
94 self.assertIsNone(root.get_child_content('e2'))
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)
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')
107 for key, value in update.items():
108 root[key] = value
110 for key, value in update.items():
111 self.assertEqual(value, root.get_child_content(key))
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)
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())
137 def test_setter_child_list_tuple(self):
138 """Tests list/tuple are appended as child to root."""
139 root = api.NaElement('root')
141 root['l'] = ['l1', 'l2']
142 root['t'] = ('t1', 't2')
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)
149 self.assertEqual(2, len(li.get_children()))
150 for le in li.get_children():
151 self.assertIn(le.get_name(), ['l1', 'l2'])
153 self.assertEqual(2, len(t.get_children()))
154 for te in t.get_children():
155 self.assertIn(te.get_name(), ['t1', 't2'])
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'))
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'))
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')
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()
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()
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()
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))
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))
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()
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"""
261 self.assertRaises(ValueError, self.root.invoke_elem, na_element)
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()))
274 self.assertRaises(api.NaApiError, self.root.invoke_elem,
275 na_element)
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()))
288 self.assertRaises(exception.StorageCommunicationException,
289 self.root.invoke_elem,
290 na_element)
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))
303 exception = self.assertRaises(api.NaApiError, self.root.invoke_elem,
304 na_element)
305 self.assertEqual('unknown', exception.code)
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))
329 response = mock.Mock()
330 response.text = 'res1'
331 self.mock_object(
332 self.root._session, 'post', mock.Mock(
333 return_value=response))
335 self.root.invoke_elem(na_element)
337 expected_log_count = 2 if log else 0
338 self.assertEqual(expected_log_count, api.LOG.debug.call_count)
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())
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()
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)
358 def _setup_mocks_for_invoke_element(self, mock_post_action):
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))
371 return fake.FAKE_BASE_URL
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()))
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)
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()))
393 self.assertRaises(exception.StorageCommunicationException,
394 self.root.invoke_elem,
395 na_element)
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)
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))
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)
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
455 api_args = {
456 "body": body,
457 "query": query
458 }
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))
476 result = self.root.invoke_elem(na_element, api_args=api_args)
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)
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())
504 method, url = self.root._get_request_info(api_name, self.root._session)
506 self.assertEqual(method, getattr(self.root._session, expected_method))
507 self.assertEqual(expected_url, url)
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
520 host_formated_for_url = (
521 '[%s]' % self.root._host if is_ipv6 else self.root._host)
523 # example of the expected format: http://10.0.0.3:80/api/
524 expected_result = (
525 protocol + '://' + host_formated_for_url + ':' + port + '/api/')
527 base_url = self.root._get_base_url()
529 self.assertEqual(expected_result, base_url)
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
538 formatted_url = self.root._add_query_params_to_url(
539 url, fake.FAKE_HTTP_QUERY)
541 self.assertEqual(expected_formated_url, formatted_url)
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())