Coverage for manila/share/drivers/netapp/dataontap/client/api.py: 66%
560 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 Navneet Singh. All rights reserved.
2# Copyright (c) 2014 Clinton Knight. 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"""
16NetApp API for Data ONTAP and OnCommand DFM.
18Contains classes required to issue API calls to Data ONTAP and OnCommand DFM.
19"""
21import copy
22import re
24from lxml import etree
25from oslo_log import log
26from oslo_serialization import jsonutils
27from oslo_utils import netutils
28import requests
29from requests.adapters import HTTPAdapter
30from requests import auth
31from requests.packages.urllib3.util.retry import Retry
33from manila import exception
34from manila.i18n import _
35from manila.share.drivers.netapp.dataontap.client import rest_endpoints
36from manila.share.drivers.netapp import utils
38LOG = log.getLogger(__name__)
40EONTAPI_EINVAL = '22'
41EVOLOPNOTSUPP = '160'
42EAPIERROR = '13001'
43EAPINOTFOUND = '13005'
44ESNAPSHOTNOTALLOWED = '13023'
45EVOLUMEDOESNOTEXIST = '13040'
46EVOLUMEOFFLINE = '13042'
47EINTERNALERROR = '13114'
48EINVALIDINPUTERROR = '13115'
49EDUPLICATEENTRY = '13130'
50EVOLUMENOTONLINE = '13157'
51EVOLNOTCLONE = '13170'
52EVOLOPNOTUNDERWAY = '13171'
53EVOLMOVE_CANNOT_MOVE_TO_CFO = '13633'
54EAGGRDOESNOTEXIST = '14420'
55EVOL_NOT_MOUNTED = '14716'
56EVSERVERALREADYSTARTED = '14923'
57ESIS_CLONE_NOT_LICENSED = '14956'
58EOBJECTNOTFOUND = '15661'
59EVSERVERNOTFOUND = '15698'
60EVOLDEL_NOT_ALLOW_BY_CLONE = '15894'
61E_VIFMGR_PORT_ALREADY_ASSIGNED_TO_BROADCAST_DOMAIN = '18605'
62EPARENTNOTONLINE = '17003'
63ERELATION_EXISTS = '17122'
64ENOTRANSFER_IN_PROGRESS = '17130'
65ETRANSFER_IN_PROGRESS = '17137'
66EANOTHER_OP_ACTIVE = '17131'
67ERELATION_NOT_QUIESCED = '17127'
68ESOURCE_IS_DIFFERENT = '17105'
69EVOL_CLONE_BEING_SPLIT = '17151'
70EPOLICYNOTFOUND = '18251'
71EEVENTNOTFOUND = '18253'
72ESCOPENOTFOUND = '18259'
73ESVMDR_CANNOT_PERFORM_OP_FOR_STATUS = '18815'
74OPERATION_ALREADY_ENABLED = '40043'
75ENFS_V4_0_ENABLED_MIGRATION_FAILURE = '13172940'
76EVSERVER_MIGRATION_TO_NON_AFF_CLUSTER = '13172984'
78STYLE_LOGIN_PASSWORD = 'basic_auth'
79TRANSPORT_TYPE_HTTP = 'http'
80TRANSPORT_TYPE_HTTPS = 'https'
81STYLE_CERTIFICATE = 'certificate_auth'
84class BaseClient(object):
85 """Encapsulates server connection logic."""
87 def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP,
88 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None,
89 username=None, password=None, port=None,
90 trace=False, api_trace_pattern=None, private_key_file=None,
91 certificate_file=None, ca_certificate_file=None,
92 certificate_host_validation=False):
93 super(BaseClient, self).__init__()
94 self._host = host
95 if private_key_file and certificate_file:
96 transport_type = TRANSPORT_TYPE_HTTPS
97 style = STYLE_CERTIFICATE
98 self.set_transport_type(transport_type)
99 self.set_style(style)
100 if port:
101 self.set_port(port)
102 self._username = username
103 self._password = password
104 self._trace = trace
105 self._api_trace_pattern = api_trace_pattern
106 self._refresh_conn = True
107 if ssl_cert_path is not None:
108 self._ssl_verify = ssl_cert_path
109 else:
110 # Note(felipe_rodrigues): it will verify with the mozila CA roots,
111 # given by certifi package.
112 self._ssl_verify = True
113 self._private_key_file = private_key_file
114 self._certificate_file = certificate_file
115 self._ca_certificate_file = ca_certificate_file
116 self._certificate_host_validation = certificate_host_validation
117 LOG.debug('Using NetApp controller: %s', self._host)
119 def get_style(self):
120 """Get the authorization style for communicating with the server."""
121 return self._auth_style
123 def set_style(self, style):
124 """Set the authorization style for communicating with the server.
126 Supports basic_auth for now. Certificate_auth mode to be done.
127 """
128 if style.lower() not in (STYLE_LOGIN_PASSWORD, STYLE_CERTIFICATE): 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 raise ValueError('Unsupported authentication style')
130 self._auth_style = style.lower()
132 def get_transport_type(self):
133 """Get the transport type protocol."""
134 return self._protocol
136 def set_transport_type(self, transport_type):
137 """Set the transport type protocol for API.
139 Supports http and https transport types.
140 """
141 if transport_type.lower() not in ( 141 ↛ 143line 141 didn't jump to line 143 because the condition on line 141 was never true
142 TRANSPORT_TYPE_HTTP, TRANSPORT_TYPE_HTTPS):
143 raise ValueError('Unsupported transport type')
144 self._protocol = transport_type.lower()
145 self._refresh_conn = True
147 def get_server_type(self):
148 """Get the server type."""
149 return self._server_type
151 def set_server_type(self, server_type):
152 """Set the target server type.
154 Supports filer and dfm server types.
155 """
156 raise NotImplementedError()
158 def set_api_version(self, major, minor):
159 """Set the API version."""
160 try:
161 self._api_major_version = int(major)
162 self._api_minor_version = int(minor)
163 self._api_version = (str(major) + "." +
164 str(minor))
165 except ValueError:
166 raise ValueError('Major and minor versions must be integers')
167 self._refresh_conn = True
169 def set_system_version(self, system_version):
170 """Set the ONTAP system version."""
171 self._system_version = system_version
172 self._refresh_conn = True
174 def get_api_version(self):
175 """Gets the API version tuple."""
176 if hasattr(self, '_api_version'):
177 return (self._api_major_version, self._api_minor_version)
178 return None
180 def get_system_version(self):
181 """Gets the ONTAP system version."""
182 if hasattr(self, '_system_version'):
183 return self._system_version
184 return None
186 def set_port(self, port):
187 """Set the server communication port."""
188 try:
189 int(port)
190 except ValueError:
191 raise ValueError('Port must be integer')
192 self._port = str(port)
193 self._refresh_conn = True
195 def get_port(self):
196 """Get the server communication port."""
197 return self._port
199 def set_timeout(self, seconds):
200 """Sets the timeout in seconds."""
201 try:
202 self._timeout = int(seconds)
203 except ValueError:
204 raise ValueError('timeout in seconds must be integer')
206 def get_timeout(self):
207 """Gets the timeout in seconds if set."""
208 if hasattr(self, '_timeout'):
209 return self._timeout
210 return None
212 def get_vserver(self):
213 """Get the vserver to use in tunneling."""
214 return self._vserver
216 def set_vserver(self, vserver):
217 """Set the vserver to use if tunneling gets enabled."""
218 self._vserver = vserver
220 def set_username(self, username):
221 """Set the user name for authentication."""
222 self._username = username
223 self._refresh_conn = True
225 def set_password(self, password):
226 """Set the password for authentication."""
227 self._password = password
228 self._refresh_conn = True
230 def invoke_successfully(self, na_element, api_args=None,
231 enable_tunneling=False, use_zapi=True):
232 """Invokes API and checks execution status as success.
234 Need to set enable_tunneling to True explicitly to achieve it.
235 This helps to use same connection instance to enable or disable
236 tunneling. The vserver or vfiler should be set before this call
237 otherwise tunneling remains disabled.
238 """
239 pass
241 def _build_session(self):
242 """Builds a session in the client."""
243 self._session = requests.Session()
245 max_retries = Retry(total=5, connect=5, read=2, backoff_factor=1)
246 adapter = HTTPAdapter(max_retries=max_retries)
247 self._session.mount('%s://' % self._protocol, adapter)
249 if self._auth_style == STYLE_CERTIFICATE:
250 self._session.cert, self._session.verify = (
251 self._create_certificate_auth_handler())
252 else:
253 self._session.auth = self._create_basic_auth_handler()
254 self._session.verify = self._ssl_verify
255 headers = self._build_headers()
257 self._session.headers = headers
259 def _build_headers(self):
260 """Adds the necessary headers to the session."""
261 raise NotImplementedError()
263 def _create_basic_auth_handler(self):
264 """Creates and returns a basic HTTP auth handler."""
265 return auth.HTTPBasicAuth(self._username, self._password)
267 def _create_certificate_auth_handler(self):
268 """Creates and returns a certificate auth handler."""
269 self._session.verify = self._certificate_host_validation
270 if self._certificate_file and self._private_key_file: 270 ↛ 275line 270 didn't jump to line 275 because the condition on line 270 was always true
271 self._session.cert = (self._certificate_file,
272 self._private_key_file)
273 # Assigning _session.verify to ca cert file to validate the certs
274 # when we have host validation set to true
275 if self._certificate_host_validation and self._ca_certificate_file:
276 self._session.verify = self._ca_certificate_file
277 return self._session.cert, self._session.verify
279 def __str__(self):
280 """Gets a representation of the client."""
281 return "server: %s" % (self._host)
284class ZapiClient(BaseClient):
286 SERVER_TYPE_FILER = 'filer'
287 SERVER_TYPE_DFM = 'dfm'
288 URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer'
289 URL_DFM = 'apis/XMLrequest'
290 NETAPP_NS = 'http://www.netapp.com/filer/admin'
292 def __init__(self, host, server_type=SERVER_TYPE_FILER,
293 transport_type=TRANSPORT_TYPE_HTTP,
294 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None,
295 password=None, port=None, trace=False,
296 api_trace_pattern=utils.API_TRACE_PATTERN,
297 private_key_file=None,
298 certificate_file=None, ca_certificate_file=None,
299 certificate_host_validation=None):
300 super(ZapiClient, self).__init__(
301 host, transport_type=transport_type, style=style,
302 ssl_cert_path=ssl_cert_path, username=username, password=password,
303 port=port, trace=trace, api_trace_pattern=api_trace_pattern,
304 private_key_file=private_key_file,
305 certificate_file=certificate_file,
306 ca_certificate_file=ca_certificate_file,
307 certificate_host_validation=certificate_host_validation)
308 self.set_server_type(server_type)
309 if port is None:
310 # Not yet set in parent, use defaults
311 self._set_port()
313 def _set_port(self):
314 """Defines which port will be used to communicate with ONTAP."""
315 if self._protocol == TRANSPORT_TYPE_HTTP: 315 ↛ 321line 315 didn't jump to line 321 because the condition on line 315 was always true
316 if self._server_type == ZapiClient.SERVER_TYPE_FILER: 316 ↛ 319line 316 didn't jump to line 319 because the condition on line 316 was always true
317 self.set_port(80)
318 else:
319 self.set_port(8088)
320 else:
321 if self._server_type == ZapiClient.SERVER_TYPE_FILER:
322 self.set_port(443)
323 else:
324 self.set_port(8488)
326 def set_server_type(self, server_type):
327 """Set the target server type.
329 Supports filer and dfm server types.
330 """
331 if server_type.lower() not in (ZapiClient.SERVER_TYPE_FILER, 331 ↛ 333line 331 didn't jump to line 333 because the condition on line 331 was never true
332 ZapiClient.SERVER_TYPE_DFM):
333 raise ValueError('Unsupported server type')
334 self._server_type = server_type.lower()
335 if self._server_type == ZapiClient.SERVER_TYPE_FILER: 335 ↛ 338line 335 didn't jump to line 338 because the condition on line 335 was always true
336 self._url = ZapiClient.URL_FILER
337 else:
338 self._url = ZapiClient.URL_DFM
339 self._ns = ZapiClient.NETAPP_NS
340 self._refresh_conn = True
342 def get_vfiler(self):
343 """Get the vfiler to use in tunneling."""
344 return self._vfiler
346 def set_vfiler(self, vfiler):
347 """Set the vfiler to use if tunneling gets enabled."""
348 self._vfiler = vfiler
350 def invoke_elem(self, na_element, enable_tunneling=False):
351 """Invoke the API on the server."""
352 if na_element and not isinstance(na_element, NaElement):
353 ValueError('NaElement must be supplied to invoke API')
355 request_element = self._create_request(na_element, enable_tunneling)
356 request_d = request_element.to_string()
358 api_name = na_element.get_name()
359 api_name_matches_regex = (re.match(self._api_trace_pattern, api_name)
360 is not None)
362 if self._trace and api_name_matches_regex:
363 LOG.debug("Request: %s", request_element.to_string(pretty=True))
365 if (not hasattr(self, '_session') or not self._session 365 ↛ 368line 365 didn't jump to line 368 because the condition on line 365 was always true
366 or self._refresh_conn):
367 self._build_session()
368 try:
369 if hasattr(self, '_timeout'): 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true
370 if self._timeout is None:
371 self._timeout = 10
372 response = self._session.post(
373 self._get_url(), data=request_d, timeout=self._timeout)
374 else:
375 response = self._session.post(
376 self._get_url(), data=request_d)
377 except requests.HTTPError as e:
378 raise NaApiError(e.errno, e.strerror)
379 except requests.URLRequired as e:
380 raise exception.StorageCommunicationException(str(e))
381 except Exception as e:
382 raise NaApiError(message=e)
384 response_xml = response.text
385 response_element = self._get_result(
386 bytes(bytearray(response_xml, encoding='utf-8')))
388 if self._trace and api_name_matches_regex:
389 LOG.debug("Response: %s", response_element.to_string(pretty=True))
391 return response_element
393 def invoke_successfully(self, na_element, api_args=None,
394 enable_tunneling=False, use_zapi=True):
395 """Invokes API and checks execution status as success.
397 Need to set enable_tunneling to True explicitly to achieve it.
398 This helps to use same connection instance to enable or disable
399 tunneling. The vserver or vfiler should be set before this call
400 otherwise tunneling remains disabled.
401 """
402 if api_args: 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true
403 na_element.translate_struct(api_args)
405 result = self.invoke_elem(
406 na_element, enable_tunneling=enable_tunneling)
408 if result.has_attr('status') and result.get_attr('status') == 'passed':
409 return result
410 code = (result.get_attr('errno')
411 or result.get_child_content('errorno')
412 or 'ESTATUSFAILED')
413 if code == ESIS_CLONE_NOT_LICENSED:
414 msg = 'Clone operation failed: FlexClone not licensed.'
415 else:
416 msg = (result.get_attr('reason')
417 or result.get_child_content('reason')
418 or 'Execution status is failed due to unknown reason')
419 raise NaApiError(code, msg)
421 def _create_request(self, na_element, enable_tunneling=False):
422 """Creates request in the desired format."""
423 netapp_elem = NaElement('netapp')
424 netapp_elem.add_attr('xmlns', self._ns)
425 if hasattr(self, '_api_version'):
426 netapp_elem.add_attr('version', self._api_version)
427 if enable_tunneling: 427 ↛ 428line 427 didn't jump to line 428 because the condition on line 427 was never true
428 self._enable_tunnel_request(netapp_elem)
429 netapp_elem.add_child_elem(na_element)
430 return netapp_elem
432 def _enable_tunnel_request(self, netapp_elem):
433 """Enables vserver or vfiler tunneling."""
434 if hasattr(self, '_vfiler') and self._vfiler:
435 if (hasattr(self, '_api_major_version') and
436 hasattr(self, '_api_minor_version') and
437 self._api_major_version >= 1 and
438 self._api_minor_version >= 7):
439 netapp_elem.add_attr('vfiler', self._vfiler)
440 else:
441 raise ValueError('ontapi version has to be atleast 1.7'
442 ' to send request to vfiler')
443 if hasattr(self, '_vserver') and self._vserver:
444 if (hasattr(self, '_api_major_version') and
445 hasattr(self, '_api_minor_version') and
446 self._api_major_version >= 1 and
447 self._api_minor_version >= 15):
448 netapp_elem.add_attr('vfiler', self._vserver)
449 else:
450 raise ValueError('ontapi version has to be atleast 1.15'
451 ' to send request to vserver')
453 @staticmethod
454 def _parse_response(response):
455 """Get the NaElement for the response."""
456 if not response:
457 raise NaApiError('No response received')
458 xml = etree.XML(response)
459 return NaElement(xml)
461 def _get_result(self, response):
462 """Gets the call result."""
463 processed_response = self._parse_response(response)
464 return processed_response.get_child_by_name('results')
466 def _get_url(self):
467 """Get the base url to send the request."""
468 host = netutils.escape_ipv6(self._host)
469 return '%s://%s:%s/%s' % (self._protocol, host, self._port, self._url)
471 def _build_headers(self):
472 """Build and return headers."""
473 return {'Content-Type': 'text/xml'}
476class RestClient(BaseClient):
478 def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP,
479 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None,
480 password=None, port=None, trace=False,
481 api_trace_pattern=utils.API_TRACE_PATTERN,
482 private_key_file=None, certificate_file=None,
483 ca_certificate_file=None, certificate_host_validation=False):
484 super(RestClient, self).__init__(
485 host, transport_type=transport_type, style=style,
486 ssl_cert_path=ssl_cert_path, username=username, password=password,
487 port=port, trace=trace, api_trace_pattern=api_trace_pattern,
488 private_key_file=private_key_file,
489 certificate_file=certificate_file,
490 ca_certificate_file=ca_certificate_file,
491 certificate_host_validation=certificate_host_validation)
492 if port is None:
493 # Not yet set in parent, use defaults
494 self._set_port()
496 def _set_port(self):
497 if self._protocol == TRANSPORT_TYPE_HTTP: 497 ↛ 500line 497 didn't jump to line 500 because the condition on line 497 was always true
498 self.set_port(80)
499 else:
500 self.set_port(443)
502 def _get_request_info(self, api_name, session):
503 """Returns the request method and url to be used in the REST call."""
505 request_methods = {
506 'post': session.post,
507 'get': session.get,
508 'put': session.put,
509 'delete': session.delete,
510 'patch': session.patch,
511 }
512 rest_call = rest_endpoints.endpoints.get(api_name)
513 return request_methods[rest_call['method']], rest_call['url']
515 def _add_query_params_to_url(self, url, query):
516 """Populates the URL with specified filters."""
517 filters = ""
518 for k, v in query.items():
519 filters += "%(key)s=%(value)s&" % {"key": k, "value": v}
520 url += "?" + filters
521 return url
523 def invoke_elem(self, na_element, api_args=None):
524 """Invoke the API on the server."""
525 if na_element and not isinstance(na_element, NaElement):
526 raise ValueError('NaElement must be supplied to invoke API')
528 api_name = na_element.get_name()
529 api_name_matches_regex = (re.match(self._api_trace_pattern, api_name)
530 is not None)
531 data = api_args.get("body") if api_args else {}
533 if (not hasattr(self, '_session') or not self._session 533 ↛ 536line 533 didn't jump to line 536 because the condition on line 533 was always true
534 or self._refresh_conn):
535 self._build_session()
536 request_method, action_url = self._get_request_info(
537 api_name, self._session)
539 url_params = api_args.get("url_params") if api_args else None
540 if url_params: 540 ↛ 541line 540 didn't jump to line 541 because the condition on line 540 was never true
541 action_url = action_url % url_params
543 query = api_args.get("query") if api_args else None
544 if query:
545 action_url = self._add_query_params_to_url(
546 action_url, api_args['query'])
548 url = self._get_base_url() + action_url
549 data = jsonutils.dumps(data) if data else data
551 if self._trace and api_name_matches_regex:
552 message = ("Request: %(method)s %(url)s. Request body "
553 "%(body)s") % {
554 "method": request_method,
555 "url": action_url,
556 "body": api_args.get("body") if api_args else {}
557 }
558 LOG.debug(message)
560 try:
561 if hasattr(self, '_timeout'): 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true
562 response = request_method(
563 url, data=data, timeout=self._timeout)
564 else:
565 response = request_method(url, data=data)
566 except requests.HTTPError as e:
567 raise NaApiError(e.errno, e.strerror)
568 except requests.URLRequired as e:
569 raise exception.StorageCommunicationException(str(e))
570 except Exception as e:
571 raise NaApiError(message=e)
573 response = (
574 jsonutils.loads(response.content) if response.content else None)
575 if self._trace and api_name_matches_regex:
576 LOG.debug("Response: %s", response)
578 return response
580 def invoke_successfully(self, na_element, api_args=None,
581 enable_tunneling=False, use_zapi=False):
582 """Invokes API and checks execution status as success.
584 Need to set enable_tunneling to True explicitly to achieve it.
585 This helps to use same connection instance to enable or disable
586 tunneling. The vserver or vfiler should be set before this call
587 otherwise tunneling remains disabled.
588 """
589 result = self.invoke_elem(na_element, api_args=api_args)
590 if not result.get('error'):
591 return result
592 result_error = result.get('error')
593 code = (result_error.get('code')
594 or 'ESTATUSFAILED')
595 if code == ESIS_CLONE_NOT_LICENSED:
596 msg = 'Clone operation failed: FlexClone not licensed.'
597 else:
598 msg = (result_error.get('message')
599 or 'Execution status is failed due to unknown reason')
600 raise NaApiError(code, msg)
602 def _get_base_url(self):
603 """Get the base URL for REST requests."""
604 host = netutils.escape_ipv6(self._host)
605 return '%s://%s:%s/api/' % (self._protocol, host, self._port)
607 def _build_headers(self):
608 """Build and return headers for a REST request."""
609 headers = {
610 "Accept": "application/json",
611 "Content-Type": "application/json"
612 }
613 return headers
616class NaServer(object):
617 """Encapsulates server connection logic."""
619 def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP,
620 style=STYLE_LOGIN_PASSWORD, ssl_cert_path=None, username=None,
621 password=None, port=None, trace=False,
622 api_trace_pattern=utils.API_TRACE_PATTERN,
623 private_key_file=None, certificate_file=None,
624 ca_certificate_file=None, certificate_host_validation=False):
625 self.zapi_client = ZapiClient(
626 host, transport_type=transport_type, style=style,
627 ssl_cert_path=ssl_cert_path, username=username, password=password,
628 port=port, trace=trace, api_trace_pattern=api_trace_pattern,
629 private_key_file=private_key_file,
630 certificate_file=certificate_file,
631 ca_certificate_file=ca_certificate_file,
632 certificate_host_validation=certificate_host_validation)
633 self.rest_client = RestClient(
634 host, transport_type=transport_type, style=style,
635 ssl_cert_path=ssl_cert_path, username=username, password=password,
636 port=port, trace=trace, api_trace_pattern=api_trace_pattern,
637 private_key_file=private_key_file,
638 certificate_file=certificate_file,
639 ca_certificate_file=ca_certificate_file,
640 certificate_host_validation=certificate_host_validation)
641 self._host = host
643 LOG.debug('Using NetApp controller: %s', self._host)
645 def get_transport_type(self, use_zapi_client=True):
646 """Get the transport type protocol."""
647 return self.get_client(use_zapi=use_zapi_client).get_transport_type()
649 def set_transport_type(self, transport_type):
650 """Set the transport type protocol for API.
652 Supports http and https transport types.
653 """
654 self.zapi_client.set_transport_type(transport_type)
655 self.rest_client.set_transport_type(transport_type)
657 def get_style(self, use_zapi_client=True):
658 """Get the authorization style for communicating with the server."""
659 return self.get_client(use_zapi=use_zapi_client).get_style()
661 def set_style(self, style):
662 """Set the authorization style for communicating with the server.
664 Supports basic_auth for now. Certificate_auth mode to be done.
665 """
666 self.zapi_client.set_style(style)
667 self.rest_client.set_style(style)
669 def get_server_type(self, use_zapi_client=True):
670 """Get the target server type."""
671 return self.get_client(use_zapi=use_zapi_client).get_server_type()
673 def set_server_type(self, server_type):
674 """Set the target server type.
676 Supports filer and dfm server types.
677 """
678 self.zapi_client.set_server_type(server_type)
679 self.rest_client.set_server_type(server_type)
681 def set_api_version(self, major, minor):
682 """Set the API version."""
683 self.zapi_client.set_api_version(major, minor)
684 self.rest_client.set_api_version(1, 0)
686 def set_system_version(self, system_version):
687 """Set the ONTAP system version."""
688 self.zapi_client.set_system_version(system_version)
689 self.rest_client.set_system_version(system_version)
691 def get_api_version(self, use_zapi_client=True):
692 """Gets the API version tuple."""
693 return self.get_client(use_zapi=use_zapi_client).get_api_version()
695 def get_system_version(self, use_zapi_client=True):
696 """Gets the ONTAP system version."""
697 return self.get_client(use_zapi=use_zapi_client).get_system_version()
699 def set_port(self, port):
700 """Set the server communication port."""
701 self.zapi_client.set_port(port)
702 self.rest_client.set_port(port)
704 def get_port(self, use_zapi_client=True):
705 """Get the server communication port."""
706 return self.get_client(use_zapi=use_zapi_client).get_port()
708 def set_timeout(self, seconds):
709 """Sets the timeout in seconds."""
710 self.zapi_client.set_timeout(seconds)
711 self.rest_client.set_timeout(seconds)
713 def get_timeout(self, use_zapi_client=True):
714 """Gets the timeout in seconds if set."""
715 return self.get_client(use_zapi=use_zapi_client).get_timeout()
717 def get_vfiler(self):
718 """Get the vfiler to use in tunneling."""
719 return self.zapi_client.get_vfiler()
721 def set_vfiler(self, vfiler):
722 """Set the vfiler to use if tunneling gets enabled."""
723 self.zapi_client.set_vfiler(vfiler)
725 def get_vserver(self, use_zapi_client=True):
726 """Get the vserver to use in tunneling."""
727 return self.get_client(use_zapi=use_zapi_client).get_vserver()
729 def set_vserver(self, vserver):
730 """Set the vserver to use if tunneling gets enabled."""
731 self.zapi_client.set_vserver(vserver)
732 self.rest_client.set_vserver(vserver)
734 def set_username(self, username):
735 """Set the user name for authentication."""
736 self.zapi_client.set_username(username)
737 self.rest_client.set_username(username)
739 def set_password(self, password):
740 """Set the password for authentication."""
741 self.zapi_client.set_password(password)
742 self.rest_client.set_password(password)
744 def get_client(self, use_zapi=True):
745 """Chooses the client to be used in the request."""
746 if use_zapi: 746 ↛ 748line 746 didn't jump to line 748 because the condition on line 746 was always true
747 return self.zapi_client
748 return self.rest_client
750 def invoke_successfully(self, na_element, api_args=None,
751 enable_tunneling=False, use_zapi=True):
752 """Invokes API and checks execution status as success.
754 Need to set enable_tunneling to True explicitly to achieve it.
755 This helps to use same connection instance to enable or disable
756 tunneling. The vserver or vfiler should be set before this call
757 otherwise tunneling remains disabled.
758 """
759 return self.get_client(use_zapi=use_zapi).invoke_successfully(
760 na_element, api_args=api_args, enable_tunneling=enable_tunneling)
762 def __str__(self):
763 return "server: %s" % (self._host)
766class NaElement(object):
767 """Class wraps basic building block for NetApp API request."""
769 def __init__(self, name):
770 """Name of the element or etree.Element."""
771 if isinstance(name, etree._Element):
772 self._element = name
773 else:
774 self._element = etree.Element(name)
776 def get_name(self):
777 """Returns the tag name of the element."""
778 return self._element.tag
780 def set_content(self, text):
781 """Set the text string for the element."""
782 self._element.text = text
784 def get_content(self):
785 """Get the text for the element."""
786 return self._element.text
788 def add_attr(self, name, value):
789 """Add the attribute to the element."""
790 self._element.set(name, value)
792 def add_attrs(self, **attrs):
793 """Add multiple attributes to the element."""
794 for attr in attrs.keys():
795 self._element.set(attr, attrs.get(attr))
797 def add_child_elem(self, na_element):
798 """Add the child element to the element."""
799 if isinstance(na_element, NaElement):
800 self._element.append(na_element._element)
801 return
802 raise ValueError(_("Can only add elements of type NaElement."))
804 def get_child_by_name(self, name):
805 """Get the child element by the tag name."""
806 for child in self._element.iterchildren():
807 if child.tag == name or etree.QName(child.tag).localname == name:
808 return NaElement(child)
809 return None
811 def get_child_content(self, name):
812 """Get the content of the child."""
813 for child in self._element.iterchildren():
814 if child.tag == name or etree.QName(child.tag).localname == name:
815 return child.text
816 return None
818 def get_children(self):
819 """Get the children for the element."""
820 return [NaElement(el) for el in self._element.iterchildren()]
822 def has_attr(self, name):
823 """Checks whether element has attribute."""
824 attributes = self._element.attrib or {}
825 return name in attributes.keys()
827 def get_attr(self, name):
828 """Get the attribute with the given name."""
829 attributes = self._element.attrib or {}
830 return attributes.get(name)
832 def get_attr_names(self):
833 """Returns the list of attribute names."""
834 attributes = self._element.attrib or {}
835 return attributes.keys()
837 def add_new_child(self, name, content, convert=False):
838 """Add child with tag name and context.
840 Convert replaces entity refs to chars.
841 """
842 child = NaElement(name)
843 if convert: 843 ↛ 844line 843 didn't jump to line 844 because the condition on line 843 was never true
844 content = NaElement._convert_entity_refs(content)
845 child.set_content(content)
846 self.add_child_elem(child)
848 @staticmethod
849 def _convert_entity_refs(text):
850 """Converts entity refs to chars to handle etree auto conversions."""
851 text = text.replace("<", "<")
852 text = text.replace(">", ">")
853 return text
855 @staticmethod
856 def create_node_with_children(node, **children):
857 """Creates and returns named node with children."""
858 parent = NaElement(node)
859 for child in children.keys():
860 parent.add_new_child(child, children.get(child, None))
861 return parent
863 def add_node_with_children(self, node, **children):
864 """Creates named node with children."""
865 parent = NaElement.create_node_with_children(node, **children)
866 self.add_child_elem(parent)
868 def to_string(self, pretty=False, method='xml', encoding='UTF-8'):
869 """Prints the element to string."""
870 return etree.tostring(self._element, method=method, encoding=encoding,
871 pretty_print=pretty)
873 def __getitem__(self, key):
874 """Dict getter method for NaElement.
876 Returns NaElement list if present,
877 text value in case no NaElement node
878 children or attribute value if present.
879 """
881 child = self.get_child_by_name(key)
882 if child:
883 if child.get_children():
884 return child
885 else:
886 return child.get_content()
887 elif self.has_attr(key):
888 return self.get_attr(key)
889 raise KeyError(_('No element by given name %s.') % (key))
891 def __setitem__(self, key, value):
892 """Dict setter method for NaElement.
894 Accepts dict, list, tuple, str, int, float and long as valid value.
895 """
896 if key:
897 if value:
898 if isinstance(value, NaElement):
899 child = NaElement(key)
900 child.add_child_elem(value)
901 self.add_child_elem(child)
902 elif isinstance(
903 value,
904 (str, ) + (int, ) + (float, )):
905 self.add_new_child(key, str(value))
906 elif isinstance(value, (list, tuple, dict)):
907 child = NaElement(key)
908 child.translate_struct(value)
909 self.add_child_elem(child)
910 else:
911 raise TypeError(_('Not a valid value for NaElement.'))
912 else:
913 self.add_child_elem(NaElement(key))
914 else:
915 raise KeyError(_('NaElement name cannot be null.'))
917 def translate_struct(self, data_struct):
918 """Convert list, tuple, dict to NaElement and appends.
920 Example usage:
921 1.
922 <root>
923 <elem1>vl1</elem1>
924 <elem2>vl2</elem2>
925 <elem3>vl3</elem3>
926 </root>
927 The above can be achieved by doing
928 root = NaElement('root')
929 root.translate_struct({'elem1': 'vl1', 'elem2': 'vl2',
930 'elem3': 'vl3'})
931 2.
932 <root>
933 <elem1>vl1</elem1>
934 <elem2>vl2</elem2>
935 <elem1>vl3</elem1>
936 </root>
937 The above can be achieved by doing
938 root = NaElement('root')
939 root.translate_struct([{'elem1': 'vl1', 'elem2': 'vl2'},
940 {'elem1': 'vl3'}])
941 """
942 if isinstance(data_struct, (list, tuple)):
943 for el in data_struct:
944 if isinstance(el, (list, tuple, dict)):
945 self.translate_struct(el)
946 else:
947 self.add_child_elem(NaElement(el))
948 elif isinstance(data_struct, dict):
949 for k in data_struct.keys():
950 child = NaElement(k)
951 if isinstance(data_struct[k], (dict, list, tuple)): 951 ↛ 952line 951 didn't jump to line 952 because the condition on line 951 was never true
952 child.translate_struct(data_struct[k])
953 else:
954 if data_struct[k]: 954 ↛ 956line 954 didn't jump to line 956 because the condition on line 954 was always true
955 child.set_content(str(data_struct[k]))
956 self.add_child_elem(child)
957 else:
958 raise ValueError(_('Type cannot be converted into NaElement.'))
961class NaApiError(Exception):
962 """Base exception class for NetApp API errors."""
964 def __init__(self, code='unknown', message='unknown'):
965 self.code = code
966 self.message = message
968 def __str__(self, *args, **kwargs):
969 return 'NetApp API failed. Reason - %s:%s' % (self.code, self.message)
972def invoke_api(na_server, api_name, api_family='cm', query=None,
973 des_result=None, additional_elems=None,
974 is_iter=False, records=0, tag=None,
975 timeout=0, tunnel=None):
976 """Invokes any given API call to a NetApp server.
978 :param na_server: na_server instance
979 :param api_name: API name string
980 :param api_family: cm or 7m
981 :param query: API query as dict
982 :param des_result: desired result as dict
983 :param additional_elems: dict other than query and des_result
984 :param is_iter: is iterator API
985 :param records: limit for records, 0 for infinite
986 :param timeout: timeout seconds
987 :param tunnel: tunnel entity, vserver or vfiler name
988 """
989 record_step = 50
990 if not (na_server or isinstance(na_server, NaServer)):
991 msg = _("Requires an NaServer instance.")
992 raise exception.InvalidInput(reason=msg)
993 server = copy.copy(na_server)
994 if api_family == 'cm':
995 server.set_vserver(tunnel)
996 else:
997 server.set_vfiler(tunnel)
998 if timeout > 0:
999 server.set_timeout(timeout)
1000 iter_records = 0
1001 cond = True
1002 while cond:
1003 na_element = create_api_request(
1004 api_name, query, des_result, additional_elems,
1005 is_iter, record_step, tag)
1006 result = server.invoke_successfully(na_element, True)
1007 if is_iter:
1008 if records > 0:
1009 iter_records = iter_records + record_step
1010 if iter_records >= records:
1011 cond = False
1012 tag_el = result.get_child_by_name('next-tag')
1013 tag = tag_el.get_content() if tag_el else None
1014 if not tag:
1015 cond = False
1016 else:
1017 cond = False
1018 yield result
1021def create_api_request(api_name, query=None, des_result=None,
1022 additional_elems=None, is_iter=False,
1023 record_step=50, tag=None):
1024 """Creates a NetApp API request.
1026 :param api_name: API name string
1027 :param query: API query as dict
1028 :param des_result: desired result as dict
1029 :param additional_elems: dict other than query and des_result
1030 :param is_iter: is iterator API
1031 :param record_step: records at a time for iter API
1032 :param tag: next tag for iter API
1033 """
1034 api_el = NaElement(api_name)
1035 if query:
1036 query_el = NaElement('query')
1037 query_el.translate_struct(query)
1038 api_el.add_child_elem(query_el)
1039 if des_result:
1040 res_el = NaElement('desired-attributes')
1041 res_el.translate_struct(des_result)
1042 api_el.add_child_elem(res_el)
1043 if additional_elems:
1044 api_el.translate_struct(additional_elems)
1045 if is_iter:
1046 api_el.add_new_child('max-records', str(record_step))
1047 if tag:
1048 api_el.add_new_child('tag', tag, True)
1049 return api_el