diff --git a/manila/share/drivers/netapp/common.py b/manila/share/drivers/netapp/common.py index 36ff021f1b..1a1a554f0d 100644 --- a/manila/share/drivers/netapp/common.py +++ b/manila/share/drivers/netapp/common.py @@ -67,7 +67,6 @@ class NetAppDriver(object): config.append_config_values(driver.share_opts) config.append_config_values(options.netapp_proxy_opts) na_utils.check_flags(NetAppDriver.REQUIRED_FLAGS, config) - na_utils.check_netapp_lib() app_version = na_utils.OpenStackInfo().info() LOG.info(_LI('OpenStack OS Version Info: %s'), app_version) diff --git a/manila/share/drivers/netapp/dataontap/client/api.py b/manila/share/drivers/netapp/dataontap/client/api.py new file mode 100644 index 0000000000..caa8bcdfc2 --- /dev/null +++ b/manila/share/drivers/netapp/dataontap/client/api.py @@ -0,0 +1,623 @@ +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +NetApp API for Data ONTAP and OnCommand DFM. + +Contains classes required to issue API calls to Data ONTAP and OnCommand DFM. +""" + +import copy + +from lxml import etree +from oslo_log import log +import six +from six.moves import urllib + +from manila import exception +from manila.i18n import _ + + +LOG = log.getLogger(__name__) + +EONTAPI_EINVAL = '22' +EAPIERROR = '13001' +EAPINOTFOUND = '13005' +ESNAPSHOTNOTALLOWED = '13023' +EVOLUMEOFFLINE = '13042' +EINTERNALERROR = '13114' +EDUPLICATEENTRY = '13130' +EVOLNOTCLONE = '13170' +EVOL_NOT_MOUNTED = '14716' +ESIS_CLONE_NOT_LICENSED = '14956' +EOBJECTNOTFOUND = '15661' +E_VIFMGR_PORT_ALREADY_ASSIGNED_TO_BROADCAST_DOMAIN = '18605' + + +class NaServer(object): + """Encapsulates server connection logic.""" + + TRANSPORT_TYPE_HTTP = 'http' + TRANSPORT_TYPE_HTTPS = 'https' + SERVER_TYPE_FILER = 'filer' + SERVER_TYPE_DFM = 'dfm' + URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer' + URL_DFM = 'apis/XMLrequest' + NETAPP_NS = 'http://www.netapp.com/filer/admin' + STYLE_LOGIN_PASSWORD = 'basic_auth' + STYLE_CERTIFICATE = 'certificate_auth' + + def __init__(self, host, server_type=SERVER_TYPE_FILER, + transport_type=TRANSPORT_TYPE_HTTP, + style=STYLE_LOGIN_PASSWORD, username=None, + password=None, port=None, trace=False): + self._host = host + self.set_server_type(server_type) + self.set_transport_type(transport_type) + self.set_style(style) + if port: + self.set_port(port) + self._username = username + self._password = password + self._trace = trace + self._refresh_conn = True + self._trace = trace + + LOG.debug('Using NetApp controller: %s', self._host) + + def get_transport_type(self): + """Get the transport type protocol.""" + return self._protocol + + def set_transport_type(self, transport_type): + """Set the transport type protocol for API. + + Supports http and https transport types. + """ + if transport_type.lower() not in ( + NaServer.TRANSPORT_TYPE_HTTP, + NaServer.TRANSPORT_TYPE_HTTPS): + raise ValueError('Unsupported transport type') + self._protocol = transport_type.lower() + if self._protocol == NaServer.TRANSPORT_TYPE_HTTP: + if self._server_type == NaServer.SERVER_TYPE_FILER: + self.set_port(80) + else: + self.set_port(8088) + else: + if self._server_type == NaServer.SERVER_TYPE_FILER: + self.set_port(443) + else: + self.set_port(8488) + self._refresh_conn = True + + def get_style(self): + """Get the authorization style for communicating with the server.""" + return self._auth_style + + def set_style(self, style): + """Set the authorization style for communicating with the server. + + Supports basic_auth for now. Certificate_auth mode to be done. + """ + if style.lower() not in (NaServer.STYLE_LOGIN_PASSWORD, + NaServer.STYLE_CERTIFICATE): + raise ValueError('Unsupported authentication style') + self._auth_style = style.lower() + + def get_server_type(self): + """Get the target server type.""" + return self._server_type + + def set_server_type(self, server_type): + """Set the target server type. + + Supports filer and dfm server types. + """ + if server_type.lower() not in (NaServer.SERVER_TYPE_FILER, + NaServer.SERVER_TYPE_DFM): + raise ValueError('Unsupported server type') + self._server_type = server_type.lower() + if self._server_type == NaServer.SERVER_TYPE_FILER: + self._url = NaServer.URL_FILER + else: + self._url = NaServer.URL_DFM + self._ns = NaServer.NETAPP_NS + self._refresh_conn = True + + def set_api_version(self, major, minor): + """Set the API version.""" + try: + self._api_major_version = int(major) + self._api_minor_version = int(minor) + self._api_version = six.text_type(major) + "." + \ + six.text_type(minor) + except ValueError: + raise ValueError('Major and minor versions must be integers') + self._refresh_conn = True + + def get_api_version(self): + """Gets the API version tuple.""" + if hasattr(self, '_api_version'): + return (self._api_major_version, self._api_minor_version) + return None + + def set_port(self, port): + """Set the server communication port.""" + try: + int(port) + except ValueError: + raise ValueError('Port must be integer') + self._port = six.text_type(port) + self._refresh_conn = True + + def get_port(self): + """Get the server communication port.""" + return self._port + + def set_timeout(self, seconds): + """Sets the timeout in seconds.""" + try: + self._timeout = int(seconds) + except ValueError: + raise ValueError('timeout in seconds must be integer') + + def get_timeout(self): + """Gets the timeout in seconds if set.""" + if hasattr(self, '_timeout'): + return self._timeout + return None + + def get_vfiler(self): + """Get the vfiler to use in tunneling.""" + return self._vfiler + + def set_vfiler(self, vfiler): + """Set the vfiler to use if tunneling gets enabled.""" + self._vfiler = vfiler + + def get_vserver(self): + """Get the vserver to use in tunneling.""" + return self._vserver + + def set_vserver(self, vserver): + """Set the vserver to use if tunneling gets enabled.""" + self._vserver = vserver + + def set_username(self, username): + """Set the user name for authentication.""" + self._username = username + self._refresh_conn = True + + def set_password(self, password): + """Set the password for authentication.""" + self._password = password + self._refresh_conn = True + + def set_trace(self, trace=True): + """Enable or disable the API tracing facility.""" + self._trace = trace + + def invoke_elem(self, na_element, enable_tunneling=False): + """Invoke the API on the server.""" + if na_element and not isinstance(na_element, NaElement): + ValueError('NaElement must be supplied to invoke API') + + request, request_element = self._create_request(na_element, + enable_tunneling) + + if self._trace: + LOG.debug("Request: %s", request_element.to_string(pretty=True)) + + if not hasattr(self, '_opener') or not self._opener \ + or self._refresh_conn: + self._build_opener() + try: + if hasattr(self, '_timeout'): + response = self._opener.open(request, timeout=self._timeout) + else: + response = self._opener.open(request) + except urllib.error.HTTPError as e: + raise NaApiError(e.code, e.msg) + except Exception as e: + raise NaApiError('Unexpected error', e) + + response_xml = response.read() + response_element = self._get_result(response_xml) + + if self._trace: + LOG.debug("Response: %s", response_element.to_string(pretty=True)) + + return response_element + + def invoke_successfully(self, na_element, enable_tunneling=False): + """Invokes API and checks execution status as success. + + Need to set enable_tunneling to True explicitly to achieve it. + This helps to use same connection instance to enable or disable + tunneling. The vserver or vfiler should be set before this call + otherwise tunneling remains disabled. + """ + result = self.invoke_elem(na_element, enable_tunneling) + if result.has_attr('status') and result.get_attr('status') == 'passed': + return result + code = result.get_attr('errno')\ + or result.get_child_content('errorno')\ + or 'ESTATUSFAILED' + if code == ESIS_CLONE_NOT_LICENSED: + msg = 'Clone operation failed: FlexClone not licensed.' + else: + msg = result.get_attr('reason')\ + or result.get_child_content('reason')\ + or 'Execution status is failed due to unknown reason' + raise NaApiError(code, msg) + + def _create_request(self, na_element, enable_tunneling=False): + """Creates request in the desired format.""" + netapp_elem = NaElement('netapp') + netapp_elem.add_attr('xmlns', self._ns) + if hasattr(self, '_api_version'): + netapp_elem.add_attr('version', self._api_version) + if enable_tunneling: + self._enable_tunnel_request(netapp_elem) + netapp_elem.add_child_elem(na_element) + request_d = netapp_elem.to_string() + request = urllib.request.Request( + self._get_url(), data=request_d, + headers={'Content-Type': 'text/xml', 'charset': 'utf-8'}) + return request, netapp_elem + + def _enable_tunnel_request(self, netapp_elem): + """Enables vserver or vfiler tunneling.""" + if hasattr(self, '_vfiler') and self._vfiler: + if hasattr(self, '_api_major_version') and \ + hasattr(self, '_api_minor_version') and \ + self._api_major_version >= 1 and \ + self._api_minor_version >= 7: + netapp_elem.add_attr('vfiler', self._vfiler) + else: + raise ValueError('ontapi version has to be atleast 1.7' + ' to send request to vfiler') + if hasattr(self, '_vserver') and self._vserver: + if hasattr(self, '_api_major_version') and \ + hasattr(self, '_api_minor_version') and \ + self._api_major_version >= 1 and \ + self._api_minor_version >= 15: + netapp_elem.add_attr('vfiler', self._vserver) + else: + raise ValueError('ontapi version has to be atleast 1.15' + ' to send request to vserver') + + def _parse_response(self, response): + """Get the NaElement for the response.""" + if not response: + raise NaApiError('No response received') + xml = etree.XML(response) + return NaElement(xml) + + def _get_result(self, response): + """Gets the call result.""" + processed_response = self._parse_response(response) + return processed_response.get_child_by_name('results') + + def _get_url(self): + return '%s://%s:%s/%s' % (self._protocol, self._host, self._port, + self._url) + + def _build_opener(self): + if self._auth_style == NaServer.STYLE_LOGIN_PASSWORD: + auth_handler = self._create_basic_auth_handler() + else: + auth_handler = self._create_certificate_auth_handler() + opener = urllib.request.build_opener(auth_handler) + self._opener = opener + + def _create_basic_auth_handler(self): + password_man = urllib.request.HTTPPasswordMgrWithDefaultRealm() + password_man.add_password(None, self._get_url(), self._username, + self._password) + auth_handler = urllib.request.HTTPBasicAuthHandler(password_man) + return auth_handler + + def _create_certificate_auth_handler(self): + raise NotImplementedError() + + def __str__(self): + return "server: %s" % (self._host) + + +class NaElement(object): + """Class wraps basic building block for NetApp API request.""" + + def __init__(self, name): + """Name of the element or etree.Element.""" + if isinstance(name, etree._Element): + self._element = name + else: + self._element = etree.Element(name) + + def get_name(self): + """Returns the tag name of the element.""" + return self._element.tag + + def set_content(self, text): + """Set the text string for the element.""" + self._element.text = text + + def get_content(self): + """Get the text for the element.""" + return self._element.text + + def add_attr(self, name, value): + """Add the attribute to the element.""" + self._element.set(name, value) + + def add_attrs(self, **attrs): + """Add multiple attributes to the element.""" + for attr in attrs.keys(): + self._element.set(attr, attrs.get(attr)) + + def add_child_elem(self, na_element): + """Add the child element to the element.""" + if isinstance(na_element, NaElement): + self._element.append(na_element._element) + return + raise + + def get_child_by_name(self, name): + """Get the child element by the tag name.""" + for child in self._element.iterchildren(): + if child.tag == name or etree.QName(child.tag).localname == name: + return NaElement(child) + return None + + def get_child_content(self, name): + """Get the content of the child.""" + for child in self._element.iterchildren(): + if child.tag == name or etree.QName(child.tag).localname == name: + return child.text + return None + + def get_children(self): + """Get the children for the element.""" + return [NaElement(el) for el in self._element.iterchildren()] + + def has_attr(self, name): + """Checks whether element has attribute.""" + attributes = self._element.attrib or {} + return name in attributes.keys() + + def get_attr(self, name): + """Get the attribute with the given name.""" + attributes = self._element.attrib or {} + return attributes.get(name) + + def get_attr_names(self): + """Returns the list of attribute names.""" + attributes = self._element.attrib or {} + return attributes.keys() + + def add_new_child(self, name, content, convert=False): + """Add child with tag name and context. + + Convert replaces entity refs to chars. + """ + child = NaElement(name) + if convert: + content = NaElement._convert_entity_refs(content) + child.set_content(content) + self.add_child_elem(child) + + @staticmethod + def _convert_entity_refs(text): + """Converts entity refs to chars to handle etree auto conversions.""" + text = text.replace("<", "<") + text = text.replace(">", ">") + return text + + @staticmethod + def create_node_with_children(node, **children): + """Creates and returns named node with children.""" + parent = NaElement(node) + for child in children.keys(): + parent.add_new_child(child, children.get(child, None)) + return parent + + def add_node_with_children(self, node, **children): + """Creates named node with children.""" + parent = NaElement.create_node_with_children(node, **children) + self.add_child_elem(parent) + + def to_string(self, pretty=False, method='xml', encoding='UTF-8'): + """Prints the element to string.""" + return etree.tostring(self._element, method=method, encoding=encoding, + pretty_print=pretty) + + def __getitem__(self, key): + """Dict getter method for NaElement. + + Returns NaElement list if present, + text value in case no NaElement node + children or attribute value if present. + """ + + child = self.get_child_by_name(key) + if child: + if child.get_children(): + return child + else: + return child.get_content() + elif self.has_attr(key): + return self.get_attr(key) + raise KeyError(_('No element by given name %s.') % (key)) + + def __setitem__(self, key, value): + """Dict setter method for NaElement. + + Accepts dict, list, tuple, str, int, float and long as valid value. + """ + if key: + if value: + if isinstance(value, NaElement): + child = NaElement(key) + child.add_child_elem(value) + self.add_child_elem(child) + elif isinstance( + value, + six.string_types + six.integer_types + (float, )): + self.add_new_child(key, six.text_type(value)) + elif isinstance(value, (list, tuple, dict)): + child = NaElement(key) + child.translate_struct(value) + self.add_child_elem(child) + else: + raise TypeError(_('Not a valid value for NaElement.')) + else: + self.add_child_elem(NaElement(key)) + else: + raise KeyError(_('NaElement name cannot be null.')) + + def translate_struct(self, data_struct): + """Convert list, tuple, dict to NaElement and appends. + + Example usage: + 1. + + vl1 + vl2 + vl3 + + The above can be achieved by doing + root = NaElement('root') + root.translate_struct({'elem1': 'vl1', 'elem2': 'vl2', + 'elem3': 'vl3'}) + 2. + + vl1 + vl2 + vl3 + + The above can be achieved by doing + root = NaElement('root') + root.translate_struct([{'elem1': 'vl1', 'elem2': 'vl2'}, + {'elem1': 'vl3'}]) + """ + if isinstance(data_struct, (list, tuple)): + for el in data_struct: + if isinstance(el, (list, tuple, dict)): + self.translate_struct(el) + else: + self.add_child_elem(NaElement(el)) + elif isinstance(data_struct, dict): + for k in data_struct.keys(): + child = NaElement(k) + if isinstance(data_struct[k], (dict, list, tuple)): + child.translate_struct(data_struct[k]) + else: + if data_struct[k]: + child.set_content(six.text_type(data_struct[k])) + self.add_child_elem(child) + else: + raise ValueError(_('Type cannot be converted into NaElement.')) + + +class NaApiError(Exception): + """Base exception class for NetApp API errors.""" + + def __init__(self, code='unknown', message='unknown'): + self.code = code + self.message = message + + def __str__(self, *args, **kwargs): + return 'NetApp API failed. Reason - %s:%s' % (self.code, self.message) + + +def invoke_api(na_server, api_name, api_family='cm', query=None, + des_result=None, additional_elems=None, + is_iter=False, records=0, tag=None, + timeout=0, tunnel=None): + """Invokes any given API call to a NetApp server. + + :param na_server: na_server instance + :param api_name: API name string + :param api_family: cm or 7m + :param query: API query as dict + :param des_result: desired result as dict + :param additional_elems: dict other than query and des_result + :param is_iter: is iterator API + :param records: limit for records, 0 for infinite + :param timeout: timeout seconds + :param tunnel: tunnel entity, vserver or vfiler name + """ + record_step = 50 + if not (na_server or isinstance(na_server, NaServer)): + msg = _("Requires an NaServer instance.") + raise exception.InvalidInput(reason=msg) + server = copy.copy(na_server) + if api_family == 'cm': + server.set_vserver(tunnel) + else: + server.set_vfiler(tunnel) + if timeout > 0: + server.set_timeout(timeout) + iter_records = 0 + cond = True + while cond: + na_element = create_api_request( + api_name, query, des_result, additional_elems, + is_iter, record_step, tag) + result = server.invoke_successfully(na_element, True) + if is_iter: + if records > 0: + iter_records = iter_records + record_step + if iter_records >= records: + cond = False + tag_el = result.get_child_by_name('next-tag') + tag = tag_el.get_content() if tag_el else None + if not tag: + cond = False + else: + cond = False + yield result + + +def create_api_request(api_name, query=None, des_result=None, + additional_elems=None, is_iter=False, + record_step=50, tag=None): + """Creates a NetApp API request. + + :param api_name: API name string + :param query: API query as dict + :param des_result: desired result as dict + :param additional_elems: dict other than query and des_result + :param is_iter: is iterator API + :param record_step: records at a time for iter API + :param tag: next tag for iter API + """ + api_el = NaElement(api_name) + if query: + query_el = NaElement('query') + query_el.translate_struct(query) + api_el.add_child_elem(query_el) + if des_result: + res_el = NaElement('desired-attributes') + res_el.translate_struct(des_result) + api_el.add_child_elem(res_el) + if additional_elems: + api_el.translate_struct(additional_elems) + if is_iter: + api_el.add_new_child('max-records', six.text_type(record_step)) + if tag: + api_el.add_new_child('tag', tag, True) + return api_el diff --git a/manila/share/drivers/netapp/dataontap/client/client_base.py b/manila/share/drivers/netapp/dataontap/client/client_base.py index eb3530c441..4982a694b5 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_base.py +++ b/manila/share/drivers/netapp/dataontap/client/client_base.py @@ -15,15 +15,11 @@ from oslo_log import log from oslo_utils import excutils -from oslo_utils import importutils from manila.i18n import _LE +from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp import utils as na_utils -netapp_lib = importutils.try_import('netapp_lib') -if netapp_lib: - from netapp_lib.api.zapi import zapi as netapp_api - LOG = log.getLogger(__name__) diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index a38905484d..45314fe7bd 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -20,21 +20,16 @@ import hashlib import time from oslo_log import log -from oslo_utils import importutils from oslo_utils import strutils from oslo_utils import units import six from manila import exception from manila.i18n import _, _LE, _LW +from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.client import client_base from manila.share.drivers.netapp import utils as na_utils -netapp_lib = importutils.try_import('netapp_lib') -if netapp_lib: - from netapp_lib.api.zapi import errors as netapp_error - from netapp_lib.api.zapi import zapi as netapp_api - LOG = log.getLogger(__name__) DELETED_PREFIX = 'deleted_manila_' @@ -217,7 +212,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: vserver_client.offline_volume(root_volume_name) except netapp_api.NaApiError as e: - if e.code == netapp_error.EVOLUMEOFFLINE: + if e.code == netapp_api.EVOLUMEOFFLINE: LOG.error(_LE("Volume %s is already offline."), root_volume_name) else: @@ -246,7 +241,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: vserver_client.send_request('cifs-server-delete', api_args) except netapp_api.NaApiError as e: - if e.code == netapp_error.EOBJECTNOTFOUND: + if e.code == netapp_api.EOBJECTNOTFOUND: LOG.error(_LE('CIFS server does not exist for ' 'Vserver %s.'), vserver_name) else: @@ -408,7 +403,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): } self.send_request('net-vlan-create', api_args) except netapp_api.NaApiError as e: - if e.code == netapp_error.EDUPLICATEENTRY: + if e.code == netapp_api.EDUPLICATEENTRY: LOG.debug('VLAN %(vlan)s already exists on port %(port)s', {'vlan': vlan, 'port': port}) else: @@ -501,7 +496,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): } self.send_request('net-port-broadcast-domain-add-ports', api_args) except netapp_api.NaApiError as e: - if e.code == (netapp_error. + if e.code == (netapp_api. E_VIFMGR_PORT_ALREADY_ASSIGNED_TO_BROADCAST_DOMAIN): LOG.debug('Port %(port)s already exists in broadcast domain ' '%(domain)s', {'port': port, 'domain': domain}) @@ -623,7 +618,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): aggrs = self._get_aggregates(aggregate_names=[aggregate_name], desired_attributes=desired_attributes) except netapp_api.NaApiError as e: - if e.code == netapp_error.EAPINOTFOUND: + if e.code == netapp_api.EAPINOTFOUND: return None else: raise e @@ -861,7 +856,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: self.send_request('kerberos-realm-create', api_args) except netapp_api.NaApiError as e: - if e.code == netapp_error.EDUPLICATEENTRY: + if e.code == netapp_api.EDUPLICATEENTRY: LOG.debug('Kerberos realm config already exists.') else: msg = _('Failed to create Kerberos realm. %s') @@ -911,7 +906,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: self.send_request('net-dns-create', api_args) except netapp_api.NaApiError as e: - if e.code == netapp_error.EDUPLICATEENTRY: + if e.code == netapp_api.EDUPLICATEENTRY: LOG.error(_LE("DNS exists for Vserver.")) else: msg = _("Failed to configure DNS. %s") @@ -1389,7 +1384,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: self.send_request('volume-offline', {'name': volume_name}) except netapp_api.NaApiError as e: - if e.code == netapp_error.EVOLUMEOFFLINE: + if e.code == netapp_api.EVOLUMEOFFLINE: return raise @@ -1403,7 +1398,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: self.send_request('volume-unmount', api_args) except netapp_api.NaApiError as e: - if e.code == netapp_error.EVOL_NOT_MOUNTED: + if e.code == netapp_api.EVOL_NOT_MOUNTED: return raise @@ -1430,7 +1425,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): LOG.debug('Volume %s unmounted.', volume_name) return except netapp_api.NaApiError as e: - if e.code == netapp_error.EAPIERROR and 'job ID' in e.message: + if e.code == netapp_api.EAPIERROR and 'job ID' in e.message: msg = _LW('Could not unmount volume %(volume)s due to ' 'ongoing volume operation: %(exception)s') msg_args = {'volume': volume_name, 'exception': e} @@ -1494,7 +1489,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): 'code': error_code, 'reason': error_reason } - if error_code == netapp_error.ESNAPSHOTNOTALLOWED: + if error_code == netapp_api.ESNAPSHOTNOTALLOWED: raise exception.SnapshotUnavailable(msg % msg_args) else: raise exception.NetAppException(msg % msg_args) @@ -1674,7 +1669,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: self.send_request('export-rule-destroy', api_args) except netapp_api.NaApiError as e: - if e.code != netapp_error.EOBJECTNOTFOUND: + if e.code != netapp_api.EOBJECTNOTFOUND: raise @na_utils.trace @@ -1744,7 +1739,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: self.send_request('export-policy-create', api_args) except netapp_api.NaApiError as e: - if e.code != netapp_error.EDUPLICATEENTRY: + if e.code != netapp_api.EDUPLICATEENTRY: raise @na_utils.trace @@ -1763,7 +1758,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): try: self.send_request('export-policy-destroy', api_args) except netapp_api.NaApiError as e: - if e.code == netapp_error.EOBJECTNOTFOUND: + if e.code == netapp_api.EOBJECTNOTFOUND: return raise @@ -1932,7 +1927,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): # API succeeded, so definitely a cluster management LIF return True except netapp_api.NaApiError as e: - if e.code == netapp_error.EAPINOTFOUND: + if e.code == netapp_api.EAPINOTFOUND: LOG.debug('Not connected to cluster management LIF.') return False else: diff --git a/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py b/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py index 8f4b8e6a9e..9f022d44de 100644 --- a/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py +++ b/manila/share/drivers/netapp/dataontap/protocols/cifs_cmode.py @@ -18,17 +18,13 @@ NetApp CIFS protocol helper class. import re from oslo_log import log -from oslo_utils import importutils from manila import exception from manila.i18n import _, _LE +from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.protocols import base from manila.share.drivers.netapp import utils as na_utils -netapp_lib = importutils.try_import('netapp_lib') -if netapp_lib: - from netapp_lib.api.zapi import errors as netapp_error - from netapp_lib.api.zapi import zapi as netapp_api LOG = log.getLogger(__name__) @@ -62,7 +58,7 @@ class NetAppCmodeCIFSHelper(base.NetAppBaseHelper): try: self._client.add_cifs_share_access(share_name, access['access_to']) except netapp_api.NaApiError as e: - if e.code == netapp_error.EDUPLICATEENTRY: + if e.code == netapp_api.EDUPLICATEENTRY: # Duplicate entry, so use specific exception. raise exception.ShareAccessExists( access_type=access['access_type'], access=access) @@ -76,9 +72,9 @@ class NetAppCmodeCIFSHelper(base.NetAppBaseHelper): try: self._client.remove_cifs_share_access(share_name, user_name) except netapp_api.NaApiError as e: - if e.code == netapp_error.EONTAPI_EINVAL: + if e.code == netapp_api.EONTAPI_EINVAL: LOG.error(_LE("User %s does not exist."), user_name) - elif e.code == netapp_error.EOBJECTNOTFOUND: + elif e.code == netapp_api.EOBJECTNOTFOUND: LOG.error(_LE("Rule %s does not exist."), user_name) else: raise e diff --git a/manila/share/drivers/netapp/utils.py b/manila/share/drivers/netapp/utils.py index 95022eb14d..90bc30aa0c 100644 --- a/manila/share/drivers/netapp/utils.py +++ b/manila/share/drivers/netapp/utils.py @@ -21,7 +21,6 @@ import platform from oslo_concurrency import processutils as putils from oslo_log import log -from oslo_utils import importutils import six from manila import exception @@ -103,14 +102,6 @@ def convert_to_list(value): return [value] -def check_netapp_lib(): - if not importutils.try_import('netapp_lib'): - msg = ('You have not installed the NetApp API Library for OpenStack. ' - 'Please install it using "sudo pip install netapp-lib" and ' - 'restart this service!') - raise exception.NetAppException(msg) - - class OpenStackInfo(object): """OS/distribution, release, and version. diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fake_api.py b/manila/tests/share/drivers/netapp/dataontap/client/fake_api.py deleted file mode 100644 index 4576452e9d..0000000000 --- a/manila/tests/share/drivers/netapp/dataontap/client/fake_api.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright (c) 2015 Clinton Knight. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import sys - -from lxml import etree -import mock -import six - -from manila import exception - - -EONTAPI_EINVAL = '22' -EAPIERROR = '13001' -EAPINOTFOUND = '13005' -ESNAPSHOTNOTALLOWED = '13023' -EVOLUMEOFFLINE = '13042' -EINTERNALERROR = '13114' -EDUPLICATEENTRY = '13130' -EVOLNOTCLONE = '13170' -EVOL_NOT_MOUNTED = '14716' -ESIS_CLONE_NOT_LICENSED = '14956' -EOBJECTNOTFOUND = '15661' -E_VIFMGR_PORT_ALREADY_ASSIGNED_TO_BROADCAST_DOMAIN = '18605' - - -def mock_netapp_lib(modules): - """Inject fake netapp_lib module classes.""" - - netapp_lib = mock.Mock() - netapp_lib.api.zapi.zapi.NaElement = NaElement - netapp_lib.api.zapi.zapi.NaApiError = NaApiError - netapp_lib.api.zapi.zapi.NaServer = mock.Mock() - netapp_lib.api.zapi.errors = sys.modules[__name__] - for module in modules: - setattr(module, 'netapp_api', netapp_lib.api.zapi.zapi) - setattr(module, 'netapp_error', netapp_lib.api.zapi.errors) - - -class NaApiError(exception.ManilaException): - """Fake NetApi API invocation error.""" - - def __init__(self, code=None, message=None): - if not code: - code = 'unknown' - if not message: - message = 'unknown' - self.code = code - self.message = message - super(NaApiError, self).__init__(message=message) - - -class NaElement(object): - """Fake XML wrapper class for NetApp API.""" - - def __init__(self, name): - """Name of the element or etree.Element.""" - if isinstance(name, etree._Element): - self._element = name - else: - self._element = etree.Element(name) - - def get_name(self): - """Returns the tag name of the element.""" - return self._element.tag - - def set_content(self, text): - """Set the text string for the element.""" - self._element.text = text - - def get_content(self): - """Get the text for the element.""" - return self._element.text - - def add_attr(self, name, value): - """Add the attribute to the element.""" - self._element.set(name, value) - - def add_attrs(self, **attrs): - """Add multiple attributes to the element.""" - for attr in attrs.keys(): - self._element.set(attr, attrs.get(attr)) - - def add_child_elem(self, na_element): - """Add the child element to the element.""" - if isinstance(na_element, NaElement): - self._element.append(na_element._element) - return - raise - - def get_child_by_name(self, name): - """Get the child element by the tag name.""" - for child in self._element.iterchildren(): - if child.tag == name or etree.QName(child.tag).localname == name: - return NaElement(child) - return None - - def get_child_content(self, name): - """Get the content of the child.""" - for child in self._element.iterchildren(): - if child.tag == name or etree.QName(child.tag).localname == name: - return child.text - return None - - def get_children(self): - """Get the children for the element.""" - return [NaElement(el) for el in self._element.iterchildren()] - - def has_attr(self, name): - """Checks whether element has attribute.""" - attributes = self._element.attrib or {} - return name in attributes.keys() - - def get_attr(self, name): - """Get the attribute with the given name.""" - attributes = self._element.attrib or {} - return attributes.get(name) - - def get_attr_names(self): - """Returns the list of attribute names.""" - attributes = self._element.attrib or {} - return attributes.keys() - - def add_new_child(self, name, content, convert=False): - """Add child with tag name and context. - - Convert replaces entity refs to chars. - """ - child = NaElement(name) - if convert: - content = NaElement._convert_entity_refs(content) - child.set_content(content) - self.add_child_elem(child) - - @staticmethod - def _convert_entity_refs(text): - """Converts entity refs to chars to handle etree auto conversions.""" - text = text.replace("<", "<") - text = text.replace(">", ">") - return text - - @staticmethod - def create_node_with_children(node, **children): - """Creates and returns named node with children.""" - parent = NaElement(node) - for child in children.keys(): - parent.add_new_child(child, children.get(child, None)) - return parent - - def add_node_with_children(self, node, **children): - """Creates named node with children.""" - parent = NaElement.create_node_with_children(node, **children) - self.add_child_elem(parent) - - def to_string(self, pretty=False, method='xml', encoding='UTF-8'): - """Prints the element to string.""" - return etree.tostring(self._element, method=method, encoding=encoding, - pretty_print=pretty) - - def __getitem__(self, key): - """Dict getter method for NaElement. - - Returns NaElement list if present, - text value in case no NaElement node - children or attribute value if present. - """ - - child = self.get_child_by_name(key) - if child: - if child.get_children(): - return child - else: - return child.get_content() - elif self.has_attr(key): - return self.get_attr(key) - raise KeyError('No element by given name %s.' % key) - - def __setitem__(self, key, value): - """Dict setter method for NaElement. - - Accepts dict, list, tuple, str, int, float and long as valid value. - """ - if key: - if value: - if isinstance(value, NaElement): - child = NaElement(key) - child.add_child_elem(value) - self.add_child_elem(child) - elif isinstance(value, (str, int, float, long)): - self.add_new_child(key, six.text_type(value)) - elif isinstance(value, (list, tuple, dict)): - child = NaElement(key) - child.translate_struct(value) - self.add_child_elem(child) - else: - raise TypeError('Not a valid value for NaElement.') - else: - self.add_child_elem(NaElement(key)) - else: - raise KeyError('NaElement name cannot be null.') - - def translate_struct(self, data_struct): - """Convert list, tuple, dict to NaElement and appends.""" - - if isinstance(data_struct, (list, tuple)): - for el in data_struct: - if isinstance(el, (list, tuple, dict)): - self.translate_struct(el) - else: - self.add_child_elem(NaElement(el)) - elif isinstance(data_struct, dict): - for k in data_struct.keys(): - child = NaElement(k) - if isinstance(data_struct[k], (dict, list, tuple)): - child.translate_struct(data_struct[k]) - else: - if data_struct[k]: - child.set_content(six.text_type(data_struct[k])) - self.add_child_elem(child) - else: - raise ValueError('Type cannot be converted into NaElement.') diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_api.py b/manila/tests/share/drivers/netapp/dataontap/client/test_api.py new file mode 100644 index 0000000000..1051e79cdc --- /dev/null +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_api.py @@ -0,0 +1,156 @@ +# Copyright (c) 2014 Ben Swartzlander. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2014 Alex Meade. All rights reserved. +# Copyright (c) 2014 Bob Callaway. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Tests for NetApp API layer +""" + +from manila.share.drivers.netapp.dataontap.client import api +from manila import test + + +class NetAppApiElementTransTests(test.TestCase): + """Test case for NetApp API element translations.""" + + def test_translate_struct_dict_unique_key(self): + """Tests if dict gets properly converted to NaElements.""" + root = api.NaElement('root') + child = {'e1': 'v1', 'e2': 'v2', 'e3': 'v3'} + + root.translate_struct(child) + + self.assertEqual(3, len(root.get_children())) + for key, value in child.items(): + self.assertEqual(value, root.get_child_content(key)) + + def test_translate_struct_dict_nonunique_key(self): + """Tests if list/dict gets properly converted to NaElements.""" + root = api.NaElement('root') + child = [{'e1': 'v1', 'e2': 'v2'}, {'e1': 'v3'}] + + root.translate_struct(child) + + children = root.get_children() + self.assertEqual(3, len(children)) + for c in children: + if c.get_name() == 'e1': + self.assertIn(c.get_content(), ['v1', 'v3']) + else: + self.assertEqual('v2', c.get_content()) + + def test_translate_struct_list(self): + """Tests if list gets properly converted to NaElements.""" + root = api.NaElement('root') + child = ['e1', 'e2'] + + root.translate_struct(child) + + self.assertEqual(2, len(root.get_children())) + self.assertIsNone(root.get_child_content('e1')) + self.assertIsNone(root.get_child_content('e2')) + + def test_translate_struct_tuple(self): + """Tests if tuple gets properly converted to NaElements.""" + root = api.NaElement('root') + child = ('e1', 'e2') + + root.translate_struct(child) + + self.assertEqual(2, len(root.get_children())) + self.assertIsNone(root.get_child_content('e1')) + self.assertIsNone(root.get_child_content('e2')) + + def test_translate_invalid_struct(self): + """Tests if invalid data structure raises exception.""" + root = api.NaElement('root') + child = 'random child element' + self.assertRaises(ValueError, root.translate_struct, child) + + def test_setter_builtin_types(self): + """Tests str, int, float get converted to NaElement.""" + update = dict(e1='v1', e2='1', e3='2.0', e4='8') + root = api.NaElement('root') + + for key, value in update.items(): + root[key] = value + + for key, value in update.items(): + self.assertEqual(value, root.get_child_content(key)) + + def test_setter_na_element(self): + """Tests na_element gets appended as child.""" + root = api.NaElement('root') + root['e1'] = api.NaElement('nested') + self.assertEqual(1, len(root.get_children())) + e1 = root.get_child_by_name('e1') + self.assertIsInstance(e1, api.NaElement) + self.assertIsInstance(e1.get_child_by_name('nested'), api.NaElement) + + def test_setter_child_dict(self): + """Tests dict is appended as child to root.""" + root = api.NaElement('root') + root['d'] = {'e1': 'v1', 'e2': 'v2'} + e1 = root.get_child_by_name('d') + self.assertIsInstance(e1, api.NaElement) + sub_ch = e1.get_children() + self.assertEqual(2, len(sub_ch)) + for c in sub_ch: + self.assertIn(c.get_name(), ['e1', 'e2']) + if c.get_name() == 'e1': + self.assertEqual('v1', c.get_content()) + else: + self.assertEqual('v2', c.get_content()) + + def test_setter_child_list_tuple(self): + """Tests list/tuple are appended as child to root.""" + root = api.NaElement('root') + + root['l'] = ['l1', 'l2'] + root['t'] = ('t1', 't2') + + l = root.get_child_by_name('l') + self.assertIsInstance(l, api.NaElement) + t = root.get_child_by_name('t') + self.assertIsInstance(t, api.NaElement) + + self.assertEqual(2, len(l.get_children())) + for le in l.get_children(): + self.assertIn(le.get_name(), ['l1', 'l2']) + + self.assertEqual(2, len(t.get_children())) + for te in t.get_children(): + self.assertIn(te.get_name(), ['t1', 't2']) + + def test_setter_no_value(self): + """Tests key with None value.""" + root = api.NaElement('root') + root['k'] = None + self.assertIsNone(root.get_child_content('k')) + + def test_setter_invalid_value(self): + """Tests invalid value raises exception.""" + self.assertRaises(TypeError, + api.NaElement('root').__setitem__, + 'k', + api.NaServer('localhost')) + + def test_setter_invalid_key(self): + """Tests invalid value raises exception.""" + self.assertRaises(KeyError, + api.NaElement('root').__setitem__, + None, + 'value') diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py index 9be521bcee..3ed8fbc9e6 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_base.py @@ -17,10 +17,9 @@ import ddt import mock from oslo_log import log +from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.client import client_base from manila import test -from manila.tests.share.drivers.netapp.dataontap.client import fake_api \ - as netapp_api from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake @@ -35,9 +34,6 @@ class NetAppBaseClientTestCase(test.TestCase): 'error', mock.Mock(side_effect=mock_logger.error)) - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([client_base]) - self.client = client_base.NetAppBaseClient(**fake.CONNECTION_INFO) self.client.connection = mock.MagicMock() self.connection = self.client.connection diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py index db0984704b..cea0d27a10 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py @@ -24,11 +24,10 @@ from oslo_log import log import six from manila import exception +from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.client import client_base from manila.share.drivers.netapp.dataontap.client import client_cmode from manila import test -from manila.tests.share.drivers.netapp.dataontap.client import fake_api \ - as netapp_api from manila.tests.share.drivers.netapp.dataontap.client import fakes as fake @@ -54,9 +53,6 @@ class NetAppClientCmodeTestCase(test.TestCase): 'get_ontapi_version', mock.Mock(return_value=(1, 20))) - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([client_base, client_cmode]) - self.client = client_cmode.NetAppCmodeClient(**fake.CONNECTION_INFO) self.client.connection = mock.MagicMock() diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py index 705d1a9c13..4b664f86f7 100644 --- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py @@ -23,12 +23,11 @@ from oslo_log import log from manila import context from manila import exception +from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base from manila.share.drivers.netapp.dataontap.cluster_mode import lib_multi_svm from manila.share.drivers.netapp import utils as na_utils from manila import test -from manila.tests.share.drivers.netapp.dataontap.client \ - import fake_api as netapp_api from manila.tests.share.drivers.netapp.dataontap import fakes as fake @@ -55,9 +54,6 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): 'app_version': fake.APP_VERSION } - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([lib_multi_svm]) - self.library = lib_multi_svm.NetAppCmodeMultiSVMFileStorageLibrary( fake.DRIVER_NAME, **kwargs) self.library._client = mock.Mock() diff --git a/manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py b/manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py index 26fb731b18..84eadba298 100644 --- a/manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py +++ b/manila/tests/share/drivers/netapp/dataontap/protocols/test_cifs_cmode.py @@ -20,10 +20,9 @@ import mock from oslo_log import log from manila import exception +from manila.share.drivers.netapp.dataontap.client import api as netapp_api from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode from manila import test -from manila.tests.share.drivers.netapp.dataontap.client \ - import fake_api as netapp_api from manila.tests.share.drivers.netapp.dataontap.protocols \ import fakes as fake @@ -42,9 +41,6 @@ class NetAppClusteredCIFSHelperTestCase(test.TestCase): self.mock_context = mock.Mock() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([cifs_cmode]) - self.mock_client = mock.Mock() self.helper = cifs_cmode.NetAppCmodeCIFSHelper() self.helper.set_client(self.mock_client) diff --git a/manila/tests/share/drivers/netapp/test_common.py b/manila/tests/share/drivers/netapp/test_common.py index 8cf0cf0f77..ce250e4f69 100644 --- a/manila/tests/share/drivers/netapp/test_common.py +++ b/manila/tests/share/drivers/netapp/test_common.py @@ -37,8 +37,6 @@ class NetAppDriverFactoryTestCase(test.TestCase): mock.Mock(return_value='fake_mode')) mock_create_driver = self.mock_object(na_common.NetAppDriver, '_create_driver') - mock_check_netapp_lib = self.mock_object(na_utils, - 'check_netapp_lib') config = na_fakes.create_configuration() config.netapp_storage_family = 'fake_family' @@ -51,7 +49,6 @@ class NetAppDriverFactoryTestCase(test.TestCase): mock_get_driver_mode.assert_called_once_with('fake_family', True) mock_create_driver.assert_called_once_with('fake_family', 'fake_mode', *(), **kwargs) - mock_check_netapp_lib.assert_called_once_with() def test_new_missing_config(self): diff --git a/manila/tests/share/drivers/netapp/test_utils.py b/manila/tests/share/drivers/netapp/test_utils.py index dba8491d1b..03c812d829 100644 --- a/manila/tests/share/drivers/netapp/test_utils.py +++ b/manila/tests/share/drivers/netapp/test_utils.py @@ -21,7 +21,6 @@ import platform import mock from oslo_concurrency import processutils as putils from oslo_log import log -from oslo_utils import importutils from manila import exception from manila.share.drivers.netapp import utils as na_utils @@ -131,21 +130,6 @@ class NetAppDriverUtilsTestCase(test.TestCase): sorted(na_utils.convert_to_list({'key1': 'value1', 'key2': 'value2'}))) - def test_check_netapp_lib(self): - mock_try_import = self.mock_object(importutils, 'try_import') - - na_utils.check_netapp_lib() - - mock_try_import.assert_called_once_with('netapp_lib') - - def test_check_netapp_lib_not_found(self): - self.mock_object(importutils, - 'try_import', - mock.Mock(return_value=None)) - - self.assertRaises(exception.NetAppException, - na_utils.check_netapp_lib) - class OpenstackInfoTestCase(test.TestCase):