1# Copyright (c) 2012 NetApp, Inc. All rights reserved. 2# Copyright (c) 2014 Navneet Singh. All rights reserved. 3# Copyright (c) 2014 Glenn Gobeli. All rights reserved. 4# Copyright (c) 2014 Clinton Knight. All rights reserved. 5# Copyright (c) 2015 Alex Meade. 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""" 19NetApp API for Data ONTAP and OnCommand DFM. 20 21Contains classes required to issue API calls to Data ONTAP and OnCommand DFM. 22""" 23 24from eventlet import greenthread 25from eventlet import semaphore 26 27from lxml import etree 28from oslo_log import log as logging 29from oslo_utils import netutils 30import random 31import six 32from six.moves import urllib 33 34from cinder import exception 35from cinder.i18n import _ 36from cinder import ssh_utils 37from cinder import utils 38from cinder.volume.drivers.netapp import utils as na_utils 39 40LOG = logging.getLogger(__name__) 41 42EAPIERROR = '13001' 43EAPIPRIVILEGE = '13003' 44EAPINOTFOUND = '13005' 45ESNAPSHOTNOTALLOWED = '13023' 46ESIS_CLONE_NOT_LICENSED = '14956' 47EOBJECTNOTFOUND = '15661' 48ESOURCE_IS_DIFFERENT = '17105' 49ERELATION_EXISTS = '17122' 50ERELATION_NOT_QUIESCED = '17127' 51ENOTRANSFER_IN_PROGRESS = '17130' 52EANOTHER_OP_ACTIVE = '17131' 53ETRANSFER_IN_PROGRESS = '17137' 54 55 56class NaServer(object): 57 """Encapsulates server connection logic.""" 58 59 TRANSPORT_TYPE_HTTP = 'http' 60 TRANSPORT_TYPE_HTTPS = 'https' 61 SERVER_TYPE_FILER = 'filer' 62 SERVER_TYPE_DFM = 'dfm' 63 URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer' 64 URL_DFM = 'apis/XMLrequest' 65 NETAPP_NS = 'http://www.netapp.com/filer/admin' 66 STYLE_LOGIN_PASSWORD = 'basic_auth' 67 STYLE_CERTIFICATE = 'certificate_auth' 68 69 def __init__(self, host, server_type=SERVER_TYPE_FILER, 70 transport_type=TRANSPORT_TYPE_HTTP, 71 style=STYLE_LOGIN_PASSWORD, username=None, 72 password=None, port=None, api_trace_pattern=None): 73 self._host = host 74 self.set_server_type(server_type) 75 self.set_transport_type(transport_type) 76 self.set_style(style) 77 if port: 78 self.set_port(port) 79 self._username = username 80 self._password = password 81 self._refresh_conn = True 82 83 if api_trace_pattern is not None: 84 na_utils.setup_api_trace_pattern(api_trace_pattern) 85 86 LOG.debug('Using NetApp controller: %s', self._host) 87 88 def set_transport_type(self, transport_type): 89 """Set the transport type protocol for API. 90 91 Supports http and https transport types. 92 """ 93 if not transport_type: 94 raise ValueError('No transport type specified') 95 if transport_type.lower() not in ( 96 NaServer.TRANSPORT_TYPE_HTTP, 97 NaServer.TRANSPORT_TYPE_HTTPS): 98 raise ValueError('Unsupported transport type') 99 self._protocol = transport_type.lower() 100 if self._protocol == NaServer.TRANSPORT_TYPE_HTTP: 101 if self._server_type == NaServer.SERVER_TYPE_FILER: 102 self.set_port(80) 103 else: 104 self.set_port(8088) 105 else: 106 if self._server_type == NaServer.SERVER_TYPE_FILER: 107 self.set_port(443) 108 else: 109 self.set_port(8488) 110 self._refresh_conn = True 111 112 def set_style(self, style): 113 """Set the authorization style for communicating with the server. 114 115 Supports basic_auth for now. Certificate_auth mode to be done. 116 """ 117 if style.lower() not in (NaServer.STYLE_LOGIN_PASSWORD, 118 NaServer.STYLE_CERTIFICATE): 119 raise ValueError('Unsupported authentication style') 120 self._auth_style = style.lower() 121 122 def set_server_type(self, server_type): 123 """Set the target server type. 124 125 Supports filer and dfm server types. 126 """ 127 if server_type.lower() not in (NaServer.SERVER_TYPE_FILER, 128 NaServer.SERVER_TYPE_DFM): 129 raise ValueError('Unsupported server type') 130 self._server_type = server_type.lower() 131 if self._server_type == NaServer.SERVER_TYPE_FILER: 132 self._url = NaServer.URL_FILER 133 else: 134 self._url = NaServer.URL_DFM 135 self._ns = NaServer.NETAPP_NS 136 self._refresh_conn = True 137 138 def set_api_version(self, major, minor): 139 """Set the API version.""" 140 try: 141 self._api_major_version = int(major) 142 self._api_minor_version = int(minor) 143 self._api_version = six.text_type(major) + "." + \ 144 six.text_type(minor) 145 except ValueError: 146 raise ValueError('Major and minor versions must be integers') 147 self._refresh_conn = True 148 149 def get_api_version(self): 150 """Gets the API version tuple.""" 151 if hasattr(self, '_api_version'): 152 return (self._api_major_version, self._api_minor_version) 153 return None 154 155 def set_port(self, port): 156 """Set the server communication port.""" 157 try: 158 int(port) 159 except ValueError: 160 raise ValueError('Port must be integer') 161 self._port = six.text_type(port) 162 self._refresh_conn = True 163 164 def set_timeout(self, seconds): 165 """Sets the timeout in seconds.""" 166 try: 167 self._timeout = int(seconds) 168 except ValueError: 169 raise ValueError('timeout in seconds must be integer') 170 171 def set_vfiler(self, vfiler): 172 """Set the vfiler to use if tunneling gets enabled.""" 173 self._vfiler = vfiler 174 175 def set_vserver(self, vserver): 176 """Set the vserver to use if tunneling gets enabled.""" 177 self._vserver = vserver 178 179 @utils.trace_api(filter_function=na_utils.trace_filter_func_api) 180 def send_http_request(self, na_element, enable_tunneling=False): 181 """Invoke the API on the server.""" 182 if not na_element or not isinstance(na_element, NaElement): 183 raise ValueError('NaElement must be supplied to invoke API') 184 185 request, request_element = self._create_request(na_element, 186 enable_tunneling) 187 188 if not hasattr(self, '_opener') or not self._opener \ 189 or self._refresh_conn: 190 self._build_opener() 191 try: 192 if hasattr(self, '_timeout'): 193 response = self._opener.open(request, timeout=self._timeout) 194 else: 195 response = self._opener.open(request) 196 except urllib.error.HTTPError as e: 197 raise NaApiError(e.code, e.msg) 198 except Exception: 199 LOG.exception("Error communicating with NetApp filer.") 200 raise NaApiError('Unexpected error') 201 202 response_xml = response.read() 203 response_element = self._get_result(response_xml) 204 205 return response_element 206 207 def invoke_successfully(self, na_element, enable_tunneling=False): 208 """Invokes API and checks execution status as success. 209 210 Need to set enable_tunneling to True explicitly to achieve it. 211 This helps to use same connection instance to enable or disable 212 tunneling. The vserver or vfiler should be set before this call 213 otherwise tunneling remains disabled. 214 """ 215 result = self.send_http_request(na_element, enable_tunneling) 216 if result.has_attr('status') and result.get_attr('status') == 'passed': 217 return result 218 code = result.get_attr('errno')\ 219 or result.get_child_content('errorno')\ 220 or 'ESTATUSFAILED' 221 if code == ESIS_CLONE_NOT_LICENSED: 222 msg = 'Clone operation failed: FlexClone not licensed.' 223 else: 224 msg = result.get_attr('reason')\ 225 or result.get_child_content('reason')\ 226 or 'Execution status is failed due to unknown reason' 227 raise NaApiError(code, msg) 228 229 def send_request(self, api_name, api_args=None, enable_tunneling=True): 230 """Sends request to Ontapi.""" 231 request = NaElement(api_name) 232 if api_args: 233 request.translate_struct(api_args) 234 return self.invoke_successfully(request, enable_tunneling) 235 236 def _create_request(self, na_element, enable_tunneling=False): 237 """Creates request in the desired format.""" 238 netapp_elem = NaElement('netapp') 239 netapp_elem.add_attr('xmlns', self._ns) 240 if hasattr(self, '_api_version'): 241 netapp_elem.add_attr('version', self._api_version) 242 if enable_tunneling: 243 self._enable_tunnel_request(netapp_elem) 244 netapp_elem.add_child_elem(na_element) 245 request_d = netapp_elem.to_string() 246 request = urllib.request.Request( 247 self._get_url(), data=request_d, 248 headers={'Content-Type': 'text/xml', 'charset': 'utf-8'}) 249 return request, netapp_elem 250 251 def _enable_tunnel_request(self, netapp_elem): 252 """Enables vserver or vfiler tunneling.""" 253 if hasattr(self, '_vfiler') and self._vfiler: 254 if hasattr(self, '_api_major_version') and \ 255 hasattr(self, '_api_minor_version') and \ 256 self._api_major_version >= 1 and \ 257 self._api_minor_version >= 7: 258 netapp_elem.add_attr('vfiler', self._vfiler) 259 else: 260 raise ValueError('ontapi version has to be atleast 1.7' 261 ' to send request to vfiler') 262 if hasattr(self, '_vserver') and self._vserver: 263 if hasattr(self, '_api_major_version') and \ 264 hasattr(self, '_api_minor_version') and \ 265 self._api_major_version >= 1 and \ 266 self._api_minor_version >= 15: 267 netapp_elem.add_attr('vfiler', self._vserver) 268 else: 269 raise ValueError('ontapi version has to be atleast 1.15' 270 ' to send request to vserver') 271 272 def _parse_response(self, response): 273 """Get the NaElement for the response.""" 274 if not response: 275 raise NaApiError('No response received') 276 xml = etree.XML(response) 277 return NaElement(xml) 278 279 def _get_result(self, response): 280 """Gets the call result.""" 281 processed_response = self._parse_response(response) 282 return processed_response.get_child_by_name('results') 283 284 def _get_url(self): 285 host = self._host 286 287 if netutils.is_valid_ipv6(host): 288 host = netutils.escape_ipv6(host) 289 290 return '%s://%s:%s/%s' % (self._protocol, host, self._port, 291 self._url) 292 293 def _build_opener(self): 294 if self._auth_style == NaServer.STYLE_LOGIN_PASSWORD: 295 auth_handler = self._create_basic_auth_handler() 296 else: 297 auth_handler = self._create_certificate_auth_handler() 298 opener = urllib.request.build_opener(auth_handler) 299 self._opener = opener 300 301 def _create_basic_auth_handler(self): 302 password_man = urllib.request.HTTPPasswordMgrWithDefaultRealm() 303 password_man.add_password(None, self._get_url(), self._username, 304 self._password) 305 auth_handler = urllib.request.HTTPBasicAuthHandler(password_man) 306 return auth_handler 307 308 def _create_certificate_auth_handler(self): 309 raise NotImplementedError() 310 311 def __str__(self): 312 return "server: %s" % self._host 313 314 315class NaElement(object): 316 """Class wraps basic building block for NetApp API request.""" 317 318 def __init__(self, name): 319 """Name of the element or etree.Element.""" 320 if isinstance(name, etree._Element): 321 self._element = name 322 else: 323 self._element = etree.Element(name) 324 325 def get_name(self): 326 """Returns the tag name of the element.""" 327 return self._element.tag 328 329 def set_content(self, text): 330 """Set the text string for the element.""" 331 self._element.text = text 332 333 def get_content(self): 334 """Get the text for the element.""" 335 return self._element.text 336 337 def add_attr(self, name, value): 338 """Add the attribute to the element.""" 339 self._element.set(name, value) 340 341 def add_attrs(self, **attrs): 342 """Add multiple attributes to the element.""" 343 for attr in attrs.keys(): 344 self._element.set(attr, attrs.get(attr)) 345 346 def add_child_elem(self, na_element): 347 """Add the child element to the element.""" 348 if isinstance(na_element, NaElement): 349 self._element.append(na_element._element) 350 return 351 raise 352 353 def get_child_by_name(self, name): 354 """Get the child element by the tag name.""" 355 for child in self._element.iterchildren(): 356 if child.tag == name or etree.QName(child.tag).localname == name: 357 return NaElement(child) 358 return None 359 360 def get_child_content(self, name): 361 """Get the content of the child.""" 362 for child in self._element.iterchildren(): 363 if child.tag == name or etree.QName(child.tag).localname == name: 364 return child.text 365 return None 366 367 def get_children(self): 368 """Get the children for the element.""" 369 return [NaElement(el) for el in self._element.iterchildren()] 370 371 def has_attr(self, name): 372 """Checks whether element has attribute.""" 373 attributes = self._element.attrib or {} 374 return name in attributes.keys() 375 376 def get_attr(self, name): 377 """Get the attribute with the given name.""" 378 attributes = self._element.attrib or {} 379 return attributes.get(name) 380 381 def get_attr_names(self): 382 """Returns the list of attribute names.""" 383 attributes = self._element.attrib or {} 384 return list(attributes.keys()) 385 386 def add_new_child(self, name, content, convert=False): 387 """Add child with tag name and content. 388 389 Convert replaces entity refs to chars. 390 """ 391 child = NaElement(name) 392 if convert: 393 content = NaElement._convert_entity_refs(content) 394 child.set_content(content) 395 self.add_child_elem(child) 396 397 @staticmethod 398 def _convert_entity_refs(text): 399 """Converts entity refs to chars to handle etree auto conversions.""" 400 text = text.replace("<", "<") 401 text = text.replace(">", ">") 402 return text 403 404 @staticmethod 405 def create_node_with_children(node, **children): 406 """Creates and returns named node with children.""" 407 parent = NaElement(node) 408 for child in children.keys(): 409 parent.add_new_child(child, children.get(child, None)) 410 return parent 411 412 def add_node_with_children(self, node, **children): 413 """Creates named node with children.""" 414 parent = NaElement.create_node_with_children(node, **children) 415 self.add_child_elem(parent) 416 417 def to_string(self, pretty=False, method='xml', encoding='UTF-8'): 418 """Prints the element to string.""" 419 return etree.tostring(self._element, method=method, encoding=encoding, 420 pretty_print=pretty) 421 422 def __str__(self): 423 xml = self.to_string(pretty=True) 424 if six.PY3: 425 xml = xml.decode('utf-8') 426 return xml 427 428 def __eq__(self, other): 429 return str(self) == str(other) 430 431 def __ne__(self, other): 432 return not self.__eq__(other) 433 434 def __hash__(self): 435 return hash(str(self)) 436 437 def __repr__(self): 438 return str(self) 439 440 def __getitem__(self, key): 441 """Dict getter method for NaElement. 442 443 Returns NaElement list if present, 444 text value in case no NaElement node 445 children or attribute value if present. 446 """ 447 448 child = self.get_child_by_name(key) 449 if child: 450 if child.get_children(): 451 return child 452 else: 453 return child.get_content() 454 elif self.has_attr(key): 455 return self.get_attr(key) 456 raise KeyError(_('No element by given name %s.') % (key)) 457 458 def __setitem__(self, key, value): 459 """Dict setter method for NaElement. 460 461 Accepts dict, list, tuple, str, int, float and long as valid value. 462 """ 463 if key: 464 if value: 465 if isinstance(value, NaElement): 466 child = NaElement(key) 467 child.add_child_elem(value) 468 self.add_child_elem(child) 469 elif isinstance(value, six.integer_types + (str, float)): 470 self.add_new_child(key, six.text_type(value)) 471 elif isinstance(value, (list, tuple, dict)): 472 child = NaElement(key) 473 child.translate_struct(value) 474 self.add_child_elem(child) 475 else: 476 raise TypeError(_('Not a valid value for NaElement.')) 477 else: 478 self.add_child_elem(NaElement(key)) 479 else: 480 raise KeyError(_('NaElement name cannot be null.')) 481 482 def translate_struct(self, data_struct): 483 """Convert list, tuple, dict to NaElement and appends. 484 485 Example usage: 486 487 1. 488 489 .. code-block:: xml 490 491 <root> 492 <elem1>vl1</elem1> 493 <elem2>vl2</elem2> 494 <elem3>vl3</elem3> 495 </root> 496 497 The above can be achieved by doing 498 499 .. code-block:: python 500 501 root = NaElement('root') 502 root.translate_struct({'elem1': 'vl1', 'elem2': 'vl2', 503 'elem3': 'vl3'}) 504 505 2. 506 507 .. code-block:: xml 508 509 <root> 510 <elem1>vl1</elem1> 511 <elem2>vl2</elem2> 512 <elem1>vl3</elem1> 513 </root> 514 515 The above can be achieved by doing 516 517 .. code-block:: python 518 519 root = NaElement('root') 520 root.translate_struct([{'elem1': 'vl1', 'elem2': 'vl2'}, 521 {'elem1': 'vl3'}]) 522 """ 523 if isinstance(data_struct, (list, tuple)): 524 for el in data_struct: 525 if isinstance(el, (list, tuple, dict)): 526 self.translate_struct(el) 527 else: 528 self.add_child_elem(NaElement(el)) 529 elif isinstance(data_struct, dict): 530 for k in data_struct.keys(): 531 child = NaElement(k) 532 if isinstance(data_struct[k], (dict, list, tuple)): 533 child.translate_struct(data_struct[k]) 534 else: 535 if data_struct[k]: 536 child.set_content(six.text_type(data_struct[k])) 537 self.add_child_elem(child) 538 else: 539 raise ValueError(_('Type cannot be converted into NaElement.')) 540 541 542class NaApiError(Exception): 543 """Base exception class for NetApp API errors.""" 544 545 def __init__(self, code='unknown', message='unknown'): 546 self.code = code 547 self.message = message 548 549 def __str__(self, *args, **kwargs): 550 return 'NetApp API failed. Reason - %s:%s' % (self.code, self.message) 551 552 553class SSHUtil(object): 554 """Encapsulates connection logic and command execution for SSH client.""" 555 556 MAX_CONCURRENT_SSH_CONNECTIONS = 5 557 RECV_TIMEOUT = 3 558 CONNECTION_KEEP_ALIVE = 600 559 WAIT_ON_STDOUT_TIMEOUT = 3 560 561 def __init__(self, host, username, password, port=22): 562 self.ssh_pool = self._init_ssh_pool(host, port, username, password) 563 564 # Note(cfouts) Number of SSH connections made to the backend need to be 565 # limited. Use of SSHPool allows connections to be cached and reused 566 # instead of creating a new connection each time a command is executed 567 # via SSH. 568 self.ssh_connect_semaphore = semaphore.Semaphore( 569 self.MAX_CONCURRENT_SSH_CONNECTIONS) 570 571 def _init_ssh_pool(self, host, port, username, password): 572 return ssh_utils.SSHPool(host, 573 port, 574 self.CONNECTION_KEEP_ALIVE, 575 username, 576 password) 577 578 def execute_command(self, client, command_text, timeout=RECV_TIMEOUT): 579 LOG.debug("execute_command() - Sending command.") 580 stdin, stdout, stderr = client.exec_command(command_text) 581 stdin.close() 582 self._wait_on_stdout(stdout, timeout) 583 output = stdout.read() 584 LOG.debug("Output of length %(size)d received.", 585 {'size': len(output)}) 586 stdout.close() 587 stderr.close() 588 return output 589 590 def execute_command_with_prompt(self, 591 client, 592 command, 593 expected_prompt_text, 594 prompt_response, 595 timeout=RECV_TIMEOUT): 596 LOG.debug("execute_command_with_prompt() - Sending command.") 597 stdin, stdout, stderr = client.exec_command(command) 598 self._wait_on_stdout(stdout, timeout) 599 response = stdout.channel.recv(999) 600 if response.strip() != expected_prompt_text: 601 msg = _("Unexpected output. Expected [%(expected)s] but " 602 "received [%(output)s]") % { 603 'expected': expected_prompt_text, 604 'output': response.strip(), 605 } 606 LOG.error(msg) 607 stdin.close() 608 stdout.close() 609 stderr.close() 610 raise exception.VolumeBackendAPIException(msg) 611 else: 612 LOG.debug("execute_command_with_prompt() - Sending answer") 613 stdin.write(prompt_response + '\n') 614 stdin.flush() 615 stdin.close() 616 stdout.close() 617 stderr.close() 618 619 def _wait_on_stdout(self, stdout, timeout=WAIT_ON_STDOUT_TIMEOUT): 620 wait_time = 0.0 621 # NOTE(cfouts): The server does not always indicate when EOF is reached 622 # for stdout. The timeout exists for this reason and an attempt is made 623 # to read from stdout. 624 while not stdout.channel.exit_status_ready(): 625 # period is 10 - 25 centiseconds 626 period = random.randint(10, 25) / 100.0 627 greenthread.sleep(period) 628 wait_time += period 629 if wait_time > timeout: 630 LOG.debug("Timeout exceeded while waiting for exit status.") 631 break 632