From bdfe5fa04e8e452c7262f265255b272f8fcfaa1d Mon Sep 17 00:00:00 2001 From: Goutham Pacha Ravi Date: Fri, 25 Sep 2015 01:52:43 -0400 Subject: [PATCH] Revert use of netapp_lib from NetApp Drivers This patch cleanly reverts the changes made via the commit: e681ba2a99995dcd999e6539bb1222f8a1ac8adc cleanly and mitigates the conflicts that would occur with git-revert on the said commit. The revert is solely for changes pertaining to the use of the external library, netapp_lib. Minor code refactors from the prior change are retained. Unit test coverage has been increased for ZAPI and REST interface code in netapp/dataontap/client/api.py and netapp/eseries/client.py. Closes-Bug: #1499334 Change-Id: Icead7e168e1c7187840de87c69365d26aedd5924 (cherry picked from commit ff81307ca4c796a66f4bd584b4633db07dcf2a16) --- cinder/tests/unit/test_netapp.py | 18 - .../tests/unit/test_netapp_eseries_iscsi.py | 4 - cinder/tests/unit/test_netapp_nfs.py | 11 +- cinder/tests/unit/test_netapp_ssc.py | 4 +- .../netapp/dataontap/client/fake_api.py | 238 ------- .../drivers/netapp/dataontap/client/fakes.py | 76 +++ .../netapp/dataontap/client/test_api.py | 509 +++++++++++++++ .../dataontap/client/test_client_7mode.py | 7 +- .../dataontap/client/test_client_base.py | 6 +- .../dataontap/client/test_client_cmode.py | 6 +- .../volume/drivers/netapp/dataontap/fakes.py | 3 +- .../netapp/dataontap/test_block_7mode.py | 6 +- .../netapp/dataontap/test_block_base.py | 6 +- .../netapp/dataontap/test_block_cmode.py | 6 +- .../netapp/dataontap/test_nfs_cmode.py | 3 +- .../volume/drivers/netapp/eseries/fakes.py | 9 +- .../drivers/netapp/eseries/test_client.py | 54 +- .../unit/volume/drivers/netapp/test_common.py | 2 - .../unit/volume/drivers/netapp/test_utils.py | 16 - cinder/volume/drivers/netapp/common.py | 1 - .../drivers/netapp/dataontap/block_base.py | 6 +- .../drivers/netapp/dataontap/client/api.py | 613 ++++++++++++++++++ .../netapp/dataontap/client/client_7mode.py | 6 +- .../netapp/dataontap/client/client_base.py | 6 +- .../netapp/dataontap/client/client_cmode.py | 13 +- .../drivers/netapp/dataontap/ssc_cmode.py | 6 +- .../volume/drivers/netapp/eseries/client.py | 84 ++- cinder/volume/drivers/netapp/utils.py | 9 - 28 files changed, 1329 insertions(+), 399 deletions(-) delete mode 100644 cinder/tests/unit/volume/drivers/netapp/dataontap/client/fake_api.py create mode 100644 cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py create mode 100644 cinder/volume/drivers/netapp/dataontap/client/api.py diff --git a/cinder/tests/unit/test_netapp.py b/cinder/tests/unit/test_netapp.py index dbc4bb1e962..39334d793fc 100644 --- a/cinder/tests/unit/test_netapp.py +++ b/cinder/tests/unit/test_netapp.py @@ -23,8 +23,6 @@ from six.moves import http_client from cinder import exception from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes from cinder.volume import configuration as conf from cinder.volume.drivers.netapp import common @@ -561,10 +559,6 @@ class NetAppDirectCmodeISCSIDriverTestCase(test.TestCase): lambda a, b, c, synchronous: None) self.mock_object(utils, 'OpenStackInfo') - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([common, client_cmode, client_base]) - self.mock_object(common.na_utils, 'check_netapp_lib') - configuration = self._set_config(create_configuration()) driver = common.NetAppDriver(configuration=configuration) self.stubs.Set(http_client, 'HTTPConnection', @@ -785,10 +779,6 @@ class NetAppDriverNegativeTestCase(test.TestCase): def setUp(self): super(NetAppDriverNegativeTestCase, self).setUp() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([common]) - self.mock_object(common.na_utils, 'check_netapp_lib') - def test_incorrect_family(self): self.mock_object(utils, 'OpenStackInfo') configuration = create_configuration() @@ -1252,10 +1242,6 @@ class NetAppDirect7modeISCSIDriverTestCase_NV(test.TestCase): def _custom_setup(self): self.mock_object(utils, 'OpenStackInfo') - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([common, client_base, client_7mode]) - self.mock_object(common.na_utils, 'check_netapp_lib') - configuration = self._set_config(create_configuration()) driver = common.NetAppDriver(configuration=configuration) self.stubs.Set(http_client, 'HTTPConnection', @@ -1316,10 +1302,6 @@ class NetAppDirect7modeISCSIDriverTestCase_WV( def _custom_setup(self): self.mock_object(utils, 'OpenStackInfo') - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([common, client_base, client_7mode]) - self.mock_object(common.na_utils, 'check_netapp_lib') - configuration = self._set_config(create_configuration()) driver = common.NetAppDriver(configuration=configuration) self.stubs.Set(http_client, 'HTTPConnection', diff --git a/cinder/tests/unit/test_netapp_eseries_iscsi.py b/cinder/tests/unit/test_netapp_eseries_iscsi.py index 11b383b43c4..dfe87fad58a 100644 --- a/cinder/tests/unit/test_netapp_eseries_iscsi.py +++ b/cinder/tests/unit/test_netapp_eseries_iscsi.py @@ -668,10 +668,6 @@ class NetAppEseriesISCSIDriverTestCase(test.TestCase): def _custom_setup(self): self.mock_object(na_utils, 'OpenStackInfo') - # Inject fake netapp_lib module classes. - fakes.mock_netapp_lib([client]) - self.mock_object(common.na_utils, 'check_netapp_lib') - configuration = self._set_config(create_configuration()) self.driver = common.NetAppDriver(configuration=configuration) self.library = self.driver.library diff --git a/cinder/tests/unit/test_netapp_nfs.py b/cinder/tests/unit/test_netapp_nfs.py index 4479651d17c..4a66240708d 100644 --- a/cinder/tests/unit/test_netapp_nfs.py +++ b/cinder/tests/unit/test_netapp_nfs.py @@ -27,8 +27,6 @@ import six from cinder import exception from cinder.image import image_utils from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) from cinder import utils as cinder_utils from cinder.volume import configuration as conf from cinder.volume.drivers.netapp import common @@ -36,6 +34,7 @@ from cinder.volume.drivers.netapp.dataontap import (nfs_7mode as netapp_nfs_7mode) from cinder.volume.drivers.netapp.dataontap import (nfs_cmode as netapp_nfs_cmode) +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_7mode from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp.dataontap.client import client_cmode @@ -159,10 +158,6 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): kwargs['netapp_mode'] = 'proxy' kwargs['configuration'] = create_configuration() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([client_cmode, client_base]) - self.mock_object(common.na_utils, 'check_netapp_lib') - self.mock_object(nfs_base, 'LOG') self._driver = netapp_nfs_cmode.NetAppCmodeNfsDriver(**kwargs) self._driver.zapi_client = mock.Mock() @@ -1472,10 +1467,6 @@ class NetApp7modeNfsDriverTestCase(NetAppCmodeNfsDriverTestCase): def _custom_setup(self): self.mock_object(utils, 'OpenStackInfo') - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([client_cmode, client_base]) - self.mock_object(common.na_utils, 'check_netapp_lib') - self.mock_object(common.na_utils, 'LOG') self.mock_object(nfs_base, 'LOG') self._driver = netapp_nfs_7mode.NetApp7modeNfsDriver( diff --git a/cinder/tests/unit/test_netapp_ssc.py b/cinder/tests/unit/test_netapp_ssc.py index 55ee62adc15..4fe5cde8e05 100644 --- a/cinder/tests/unit/test_netapp_ssc.py +++ b/cinder/tests/unit/test_netapp_ssc.py @@ -25,8 +25,7 @@ from six.moves import http_client from cinder import exception from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap import ssc_cmode @@ -375,7 +374,6 @@ class SscUtilsTestCase(test.TestCase): def setUp(self): super(SscUtilsTestCase, self).setUp() - netapp_api.mock_netapp_lib([ssc_cmode]) self.stubs.Set(http_client, 'HTTPConnection', FakeDirectCmodeHTTPConnection) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fake_api.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fake_api.py deleted file mode 100644 index 71dcbbe87d1..00000000000 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fake_api.py +++ /dev/null @@ -1,238 +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 cinder 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.CinderException): - """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 NaServer(object): - """Fake XML wrapper class for NetApp Server""" - def __init__(self, host): - self._host = host - - -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/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py index 04428ccc0a1..0bfee43d093 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py @@ -14,7 +14,83 @@ from lxml import etree +import mock +from six.moves import urllib +import cinder.volume.drivers.netapp.dataontap.client.api as netapp_api + + +FAKE_VOL_XML = """ + open123 + online + 0 + 0 + 0 + false + false + """ + +FAKE_XML1 = """\ +abc\ +abc\ +""" + +FAKE_XML2 = """somecontent""" + +FAKE_NA_ELEMENT = netapp_api.NaElement(etree.XML(FAKE_VOL_XML)) + +FAKE_INVOKE_DATA = 'somecontent' + +FAKE_XML_STR = 'abc' + +FAKE_API_NAME = 'volume-get-iter' + +FAKE_API_NAME_ELEMENT = netapp_api.NaElement(FAKE_API_NAME) + +FAKE_NA_SERVER_STR = '127.0.0.1' + +FAKE_NA_SERVER = netapp_api.NaServer(FAKE_NA_SERVER_STR) + +FAKE_NA_SERVER_API_1_5 = netapp_api.NaServer(FAKE_NA_SERVER_STR) +FAKE_NA_SERVER_API_1_5.set_vfiler('filer') +FAKE_NA_SERVER_API_1_5.set_api_version(1, 5) + + +FAKE_NA_SERVER_API_1_14 = netapp_api.NaServer(FAKE_NA_SERVER_STR) +FAKE_NA_SERVER_API_1_14.set_vserver('server') +FAKE_NA_SERVER_API_1_14.set_api_version(1, 14) + + +FAKE_NA_SERVER_API_1_20 = netapp_api.NaServer(FAKE_NA_SERVER_STR) +FAKE_NA_SERVER_API_1_20.set_vfiler('filer') +FAKE_NA_SERVER_API_1_20.set_vserver('server') +FAKE_NA_SERVER_API_1_20.set_api_version(1, 20) + + +FAKE_QUERY = {'volume-attributes': None} + +FAKE_DES_ATTR = {'volume-attributes': ['volume-id-attributes', + 'volume-space-attributes', + 'volume-state-attributes', + 'volume-qos-attributes']} + +FAKE_CALL_ARGS_LIST = [mock.call(80), mock.call(8088), mock.call(443), + mock.call(8488)] + +FAKE_RESULT_API_ERR_REASON = netapp_api.NaElement('result') +FAKE_RESULT_API_ERR_REASON.add_attr('errno', '000') +FAKE_RESULT_API_ERR_REASON.add_attr('reason', 'fake_reason') + +FAKE_RESULT_API_ERRNO_INVALID = netapp_api.NaElement('result') +FAKE_RESULT_API_ERRNO_INVALID.add_attr('errno', '000') + +FAKE_RESULT_API_ERRNO_VALID = netapp_api.NaElement('result') +FAKE_RESULT_API_ERRNO_VALID.add_attr('errno', '14956') + +FAKE_RESULT_SUCCESS = netapp_api.NaElement('result') +FAKE_RESULT_SUCCESS.add_attr('status', 'passed') + +FAKE_HTTP_OPENER = urllib.request.build_opener() GET_OPERATIONAL_NETWORK_INTERFACE_ADDRESSES_RESPONSE = etree.XML(""" diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py new file mode 100644 index 00000000000..b2ff96d24a9 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py @@ -0,0 +1,509 @@ +# 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 +""" +import ddt +from lxml import etree +import mock +import six +from six.moves import urllib + +from cinder import exception +from cinder.i18n import _ +from cinder import test +from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( + fakes as zapi_fakes) +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api + + +@ddt.ddt +class NetAppApiServerTests(test.TestCase): + """Test case for NetApp API server methods""" + def setUp(self): + self.root = netapp_api.NaServer('127.0.0.1') + super(NetAppApiServerTests, self).setUp() + + @ddt.data(None, 'ftp') + def test_set_transport_type_value_error(self, transport_type): + """Tests setting an invalid transport type""" + self.assertRaises(ValueError, self.root.set_transport_type, + transport_type) + + @ddt.data({'params': {'transport_type': 'http', + 'server_type_filer': 'filer'}}, + {'params': {'transport_type': 'http', + 'server_type_filer': 'xyz'}}, + {'params': {'transport_type': 'https', + 'server_type_filer': 'filer'}}, + {'params': {'transport_type': 'https', + 'server_type_filer': 'xyz'}}) + @ddt.unpack + def test_set_transport_type_valid(self, params): + """Tests setting a valid transport type""" + self.root._server_type = params['server_type_filer'] + mock_invoke = self.mock_object(self.root, 'set_port') + + self.root.set_transport_type(params['transport_type']) + + expected_call_args = zapi_fakes.FAKE_CALL_ARGS_LIST + + self.assertTrue(mock_invoke.call_args in expected_call_args) + + @ddt.data('stor', 'STORE', '') + def test_set_server_type_value_error(self, server_type): + """Tests Value Error on setting the wrong server type""" + self.assertRaises(ValueError, self.root.set_server_type, server_type) + + @ddt.data('!&', '80na', '') + def test_set_port__value_error(self, port): + """Tests Value Error on trying to set port with a non-integer""" + self.assertRaises(ValueError, self.root.set_port, port) + + @ddt.data('!&', '80na', '') + def test_set_timeout_value_error(self, timeout): + """Tests Value Error on trying to set port with a non-integer""" + self.assertRaises(ValueError, self.root.set_timeout, timeout) + + @ddt.data({'params': {'major': 1, 'minor': '20a'}}, + {'params': {'major': '20a', 'minor': 1}}, + {'params': {'major': '!*', 'minor': '20a'}}) + @ddt.unpack + def test_set_api_version_value_error(self, params): + """Tests Value Error on setting non-integer version""" + self.assertRaises(ValueError, self.root.set_api_version, **params) + + def test_set_api_version_valid(self): + """Tests Value Error on setting non-integer version""" + args = {'major': '20', 'minor': 1} + + expected_call_args_list = [mock.call('20'), mock.call(1)] + + mock_invoke = self.mock_object(six, 'text_type', + mock.Mock(return_value='str')) + self.root.set_api_version(**args) + + self.assertEqual(expected_call_args_list, mock_invoke.call_args_list) + + @ddt.data({'params': {'result': zapi_fakes.FAKE_RESULT_API_ERR_REASON}}, + {'params': {'result': zapi_fakes.FAKE_RESULT_API_ERRNO_INVALID}}, + {'params': {'result': zapi_fakes.FAKE_RESULT_API_ERRNO_VALID}}) + @ddt.unpack + def test_invoke_successfully_naapi_error(self, params): + """Tests invoke successfully raising NaApiError""" + self.mock_object(self.root, 'invoke_elem', + mock.Mock(return_value=params['result'])) + + self.assertRaises(netapp_api.NaApiError, + self.root.invoke_successfully, + zapi_fakes.FAKE_NA_ELEMENT) + + def test_invoke_successfully_no_error(self): + """Tests invoke successfully with no errors""" + self.mock_object(self.root, 'invoke_elem', mock.Mock( + return_value=zapi_fakes.FAKE_RESULT_SUCCESS)) + + self.assertEqual(zapi_fakes.FAKE_RESULT_SUCCESS.to_string(), + self.root.invoke_successfully( + zapi_fakes.FAKE_NA_ELEMENT).to_string()) + + def test__create_request(self): + """Tests method _create_request""" + self.root._ns = zapi_fakes.FAKE_XML_STR + self.root._api_version = '1.20' + self.mock_object(self.root, '_enable_tunnel_request') + self.mock_object(netapp_api.NaElement, 'add_child_elem') + self.mock_object(netapp_api.NaElement, 'to_string', + mock.Mock(return_value=zapi_fakes.FAKE_XML_STR)) + mock_invoke = self.mock_object(urllib.request, 'Request') + + self.root._create_request(zapi_fakes.FAKE_NA_ELEMENT, True) + + self.assertTrue(mock_invoke.called) + + @ddt.data({'params': {'server': zapi_fakes.FAKE_NA_SERVER_API_1_5}}, + {'params': {'server': zapi_fakes.FAKE_NA_SERVER_API_1_14}}) + @ddt.unpack + def test__enable_tunnel_request__value_error(self, params): + """Tests value errors with creating tunnel request""" + + self.assertRaises(ValueError, params['server']._enable_tunnel_request, + 'test') + + def test__enable_tunnel_request_valid(self): + """Tests creating tunnel request with correct values""" + netapp_elem = zapi_fakes.FAKE_NA_ELEMENT + server = zapi_fakes.FAKE_NA_SERVER_API_1_20 + mock_invoke = self.mock_object(netapp_elem, 'add_attr') + expected_call_args = [mock.call('vfiler', 'filer'), + mock.call('vfiler', 'server')] + + server._enable_tunnel_request(netapp_elem) + + self.assertEqual(expected_call_args, mock_invoke.call_args_list) + + def test__parse_response__naapi_error(self): + """Tests NaApiError on no response""" + self.assertRaises(netapp_api.NaApiError, + self.root._parse_response, None) + + def test__parse_response_no_error(self): + """Tests parse function with appropriate response""" + mock_invoke = self.mock_object(etree, 'XML', mock.Mock( + return_value='xml')) + + self.root._parse_response(zapi_fakes.FAKE_XML_STR) + + mock_invoke.assert_called_with(zapi_fakes.FAKE_XML_STR) + + def test__build_opener_not_implemented_error(self): + """Tests whether certificate style authorization raises Exception""" + self.root._auth_style = 'not_basic_auth' + + self.assertRaises(NotImplementedError, self.root._build_opener) + + def test__build_opener_valid(self): + """Tests whether build opener works with valid parameters""" + self.root._auth_style = 'basic_auth' + mock_invoke = self.mock_object(urllib.request, 'build_opener') + + self.root._build_opener() + + self.assertTrue(mock_invoke.called) + + @ddt.data(None, zapi_fakes.FAKE_XML_STR) + def test_invoke_elem_value_error(self, na_element): + """Tests whether invalid NaElement parameter causes error""" + + self.assertRaises(ValueError, self.root.invoke_elem, na_element) + + def test_invoke_elem_http_error(self): + """Tests handling of HTTPError""" + na_element = zapi_fakes.FAKE_NA_ELEMENT + self.mock_object(self.root, '_create_request', mock.Mock( + return_value=('abc', zapi_fakes.FAKE_NA_ELEMENT))) + self.mock_object(netapp_api, 'LOG') + self.root._opener = zapi_fakes.FAKE_HTTP_OPENER + self.mock_object(self.root, '_build_opener') + self.mock_object(self.root._opener, 'open', mock.Mock( + side_effect=urllib.error.HTTPError(url='', hdrs='', + fp=None, code='401', + msg='httperror'))) + + self.assertRaises(netapp_api.NaApiError, self.root.invoke_elem, + na_element) + + def test_invoke_elem_unknown_exception(self): + """Tests handling of Unknown Exception""" + na_element = zapi_fakes.FAKE_NA_ELEMENT + self.mock_object(self.root, '_create_request', mock.Mock( + return_value=('abc', zapi_fakes.FAKE_NA_ELEMENT))) + self.mock_object(netapp_api, 'LOG') + self.root._opener = zapi_fakes.FAKE_HTTP_OPENER + self.mock_object(self.root, '_build_opener') + self.mock_object(self.root._opener, 'open', mock.Mock( + side_effect=Exception)) + + self.assertRaises(netapp_api.NaApiError, self.root.invoke_elem, + na_element) + + def test_invoke_elem_valid(self): + """Tests the method invoke_elem with valid parameters""" + na_element = zapi_fakes.FAKE_NA_ELEMENT + self.root._trace = True + self.mock_object(self.root, '_create_request', mock.Mock( + return_value=('abc', zapi_fakes.FAKE_NA_ELEMENT))) + self.mock_object(netapp_api, 'LOG') + self.root._opener = zapi_fakes.FAKE_HTTP_OPENER + self.mock_object(self.root, '_build_opener') + self.mock_object(self.root, '_get_result', mock.Mock( + return_value=zapi_fakes.FAKE_NA_ELEMENT)) + opener_mock = self.mock_object( + self.root._opener, 'open', mock.Mock()) + opener_mock.read.side_effect = ['resp1', 'resp2'] + + self.root.invoke_elem(na_element) + + +class NetAppApiElementTransTests(test.TestCase): + """Test case for NetApp API element translations.""" + + def setUp(self): + super(NetAppApiElementTransTests, self).setUp() + + def test_translate_struct_dict_unique_key(self): + """Tests if dict gets properly converted to NaElements.""" + root = netapp_api.NaElement('root') + child = {'e1': 'v1', 'e2': 'v2', 'e3': 'v3'} + root.translate_struct(child) + self.assertEqual(3, len(root.get_children())) + self.assertEqual('v1', root.get_child_content('e1')) + self.assertEqual('v2', root.get_child_content('e2')) + self.assertEqual('v3', root.get_child_content('e3')) + + def test_translate_struct_dict_nonunique_key(self): + """Tests if list/dict gets properly converted to NaElements.""" + root = netapp_api.NaElement('root') + child = [{'e1': 'v1', 'e2': 'v2'}, {'e1': 'v3'}] + root.translate_struct(child) + self.assertEqual(3, len(root.get_children())) + children = root.get_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 = netapp_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 = netapp_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 = netapp_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.""" + root = netapp_api.NaElement('root') + root['e1'] = 'v1' + root['e2'] = 1 + root['e3'] = 2.0 + root['e4'] = 8l + self.assertEqual(4, len(root.get_children())) + self.assertEqual('v1', root.get_child_content('e1')) + self.assertEqual('1', root.get_child_content('e2')) + self.assertEqual('2.0', root.get_child_content('e3')) + self.assertEqual('8', root.get_child_content('e4')) + + def test_setter_na_element(self): + """Tests na_element gets appended as child.""" + root = netapp_api.NaElement('root') + root['e1'] = netapp_api.NaElement('nested') + self.assertEqual(1, len(root.get_children())) + e1 = root.get_child_by_name('e1') + self.assertIsInstance(e1, netapp_api.NaElement) + self.assertIsInstance(e1.get_child_by_name('nested'), + netapp_api.NaElement) + + def test_setter_child_dict(self): + """Tests dict is appended as child to root.""" + root = netapp_api.NaElement('root') + root['d'] = {'e1': 'v1', 'e2': 'v2'} + e1 = root.get_child_by_name('d') + self.assertIsInstance(e1, netapp_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 = netapp_api.NaElement('root') + root['l'] = ['l1', 'l2'] + root['t'] = ('t1', 't2') + l = root.get_child_by_name('l') + self.assertIsInstance(l, netapp_api.NaElement) + t = root.get_child_by_name('t') + self.assertIsInstance(t, netapp_api.NaElement) + for le in l.get_children(): + self.assertIn(le.get_name(), ['l1', 'l2']) + 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 = netapp_api.NaElement('root') + root['k'] = None + self.assertIsNone(root.get_child_content('k')) + + def test_setter_invalid_value(self): + """Tests invalid value raises exception.""" + root = netapp_api.NaElement('root') + try: + root['k'] = netapp_api.NaServer('localhost') + except Exception as e: + if not isinstance(e, TypeError): + self.fail(_('Error not a TypeError.')) + + def test_setter_invalid_key(self): + """Tests invalid value raises exception.""" + root = netapp_api.NaElement('root') + try: + root[None] = 'value' + except Exception as e: + if not isinstance(e, KeyError): + self.fail(_('Error not a KeyError.')) + + def test_getter_key_error(self): + """Tests invalid key raises exception""" + root = netapp_api.NaElement('root') + self.mock_object(root, 'get_child_by_name', + mock.Mock(return_value=None)) + self.mock_object(root, 'has_attr', + mock.Mock(return_value=None)) + + self.assertRaises(KeyError, + netapp_api.NaElement.__getitem__, + root, '123') + + def test_getter_na_element_list(self): + """Tests returning NaElement list""" + root = netapp_api.NaElement('root') + root['key'] = ['val1', 'val2'] + + self.assertEqual(root.get_child_by_name('key').get_name(), + root.__getitem__('key').get_name()) + + def test_getter_child_text(self): + """Tests NaElement having no children""" + root = netapp_api.NaElement('root') + root.set_content('FAKE_CONTENT') + self.mock_object(root, 'get_child_by_name', + mock.Mock(return_value=root)) + + self.assertEqual('FAKE_CONTENT', + root.__getitem__('root')) + + def test_getter_child_attr(self): + """Tests invalid key raises exception""" + root = netapp_api.NaElement('root') + root.add_attr('val', 'FAKE_VALUE') + + self.assertEqual('FAKE_VALUE', + root.__getitem__('val')) + + def test_add_node_with_children(self): + """Tests adding a child node with its own children""" + root = netapp_api.NaElement('root') + self.mock_object(netapp_api.NaElement, + 'create_node_with_children', + mock.Mock(return_value=zapi_fakes.FAKE_INVOKE_DATA)) + mock_invoke = self.mock_object(root, 'add_child_elem') + + root.add_node_with_children('options') + + mock_invoke.assert_called_with(zapi_fakes.FAKE_INVOKE_DATA) + + def test_create_node_with_children(self): + """Tests adding a child node with its own children""" + root = netapp_api.NaElement('root') + self.mock_object(root, 'add_new_child', mock.Mock(return_value='abc')) + + self.assertEqual(zapi_fakes.FAKE_XML1, root.create_node_with_children( + 'options', test1=zapi_fakes.FAKE_XML_STR, + test2=zapi_fakes.FAKE_XML_STR).to_string()) + + def test_add_new_child(self): + """Tests adding a child node with its own children""" + root = netapp_api.NaElement('root') + self.mock_object(netapp_api.NaElement, + '_convert_entity_refs', + mock.Mock(return_value=zapi_fakes.FAKE_INVOKE_DATA)) + + root.add_new_child('options', zapi_fakes.FAKE_INVOKE_DATA) + + self.assertEqual(zapi_fakes.FAKE_XML2, root.to_string()) + + def test_get_attr_names_empty_attr(self): + """Tests _elements.attrib being empty""" + root = netapp_api.NaElement('root') + + self.assertEqual([], root.get_attr_names()) + + def test_get_attr_names(self): + """Tests _elements.attrib being non-empty""" + root = netapp_api.NaElement('root') + root.add_attr('attr1', 'a1') + root.add_attr('attr2', 'a2') + + self.assertEqual(['attr1', 'attr2'], root.get_attr_names()) + + +@ddt.ddt +class NetAppApiInvokeTests(test.TestCase): + """Test Cases for api request creation and invocation""" + + def setUp(self): + super(NetAppApiInvokeTests, self).setUp() + + @ddt.data(None, zapi_fakes.FAKE_XML_STR) + def test_invoke_api_invalid_input(self, na_server): + """Tests Zapi Invocation Type Error""" + na_server = None + api_name = zapi_fakes.FAKE_API_NAME + invoke_generator = netapp_api.invoke_api(na_server, api_name) + + self.assertRaises(exception.InvalidInput, invoke_generator.next) + + @ddt.data({'params': {'na_server': zapi_fakes.FAKE_NA_SERVER, + 'api_name': zapi_fakes.FAKE_API_NAME}}, + {'params': {'na_server': zapi_fakes.FAKE_NA_SERVER, + 'api_name': zapi_fakes.FAKE_API_NAME, + 'api_family': 'cm', + 'query': zapi_fakes.FAKE_QUERY, + 'des_result': zapi_fakes.FAKE_DES_ATTR, + 'additional_elems': None, + 'is_iter': True}}) + @ddt.unpack + def test_invoke_api_valid(self, params): + """Test invoke_api with valid naserver""" + self.mock_object(netapp_api, 'create_api_request', mock.Mock( + return_value='success')) + self.mock_object(netapp_api.NaServer, 'invoke_successfully', + mock.Mock( + return_value=netapp_api.NaElement('success'))) + + invoke_generator = netapp_api.invoke_api(**params) + + self.assertEqual(netapp_api.NaElement('success').to_string(), + invoke_generator.next().to_string()) + + def test_create_api_request(self): + """"Tests creating api request""" + self.mock_object(netapp_api.NaElement, 'translate_struct') + self.mock_object(netapp_api.NaElement, 'add_child_elem') + + params = {'api_name': zapi_fakes.FAKE_API_NAME, + 'query': zapi_fakes.FAKE_QUERY, + 'des_result': zapi_fakes.FAKE_DES_ATTR, + 'additional_elems': zapi_fakes.FAKE_XML_STR, + 'is_iter': True, + 'tag': 'tag'} + + self.assertEqual(zapi_fakes.FAKE_API_NAME_ELEMENT.to_string(), + netapp_api.create_api_request(**params).to_string()) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py index 55481ee9006..4f3467c00d0 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_7mode.py @@ -21,11 +21,9 @@ import mock import six from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_7mode -from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp import utils as netapp_utils CONNECTION_INFO = {'hostname': 'hostname', @@ -42,9 +40,6 @@ class NetApp7modeClientTestCase(test.TestCase): self.fake_volume = six.text_type(uuid.uuid4()) - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([client_7mode, netapp_utils, client_base]) - with mock.patch.object(client_7mode.Client, 'get_ontapi_version', return_value=(1, 20)): diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py index 30d8edbcb67..26763026ed0 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py @@ -20,9 +20,8 @@ import mock import six from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) import cinder.tests.unit.volume.drivers.netapp.dataontap.fakes as fake +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base @@ -38,9 +37,6 @@ class NetAppBaseClientTestCase(test.TestCase): def setUp(self): super(NetAppBaseClientTestCase, self).setUp() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([client_base]) - self.mock_object(client_base, 'LOG') self.client = client_base.Client(**CONNECTION_INFO) self.client.connection = mock.MagicMock() diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py index e5e69148e54..3e0909bfcbf 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py @@ -22,11 +22,10 @@ import six from cinder import exception from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( fakes as fake_client) from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_cmode from cinder.volume.drivers.netapp import utils as netapp_utils @@ -44,9 +43,6 @@ class NetAppCmodeClientTestCase(test.TestCase): def setUp(self): super(NetAppCmodeClientTestCase, self).setUp() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([client_cmode]) - with mock.patch.object(client_cmode.Client, 'get_ontapi_version', return_value=(1, 20)): diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index f097d4943f1..b110038d305 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -15,8 +15,7 @@ from lxml import etree -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap import ssc_cmode diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py index a217506c3a1..be20e7d6d67 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_7mode.py @@ -25,14 +25,13 @@ import mock from cinder import exception from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) import cinder.tests.unit.volume.drivers.netapp.dataontap.client.fakes \ as client_fakes import cinder.tests.unit.volume.drivers.netapp.dataontap.fakes as fake import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes from cinder.volume.drivers.netapp.dataontap import block_7mode from cinder.volume.drivers.netapp.dataontap import block_base +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp import utils as na_utils @@ -44,9 +43,6 @@ class NetAppBlockStorage7modeLibraryTestCase(test.TestCase): def setUp(self): super(NetAppBlockStorage7modeLibraryTestCase, self).setUp() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([block_7mode, client_base]) - kwargs = {'configuration': self.get_config_7mode()} self.library = block_7mode.NetAppBlockStorage7modeLibrary( 'driver', 'protocol', **kwargs) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py index 2b377bb3631..5d15706f667 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_base.py @@ -29,11 +29,10 @@ from oslo_utils import units from cinder import exception from cinder.i18n import _ from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes from cinder.volume.drivers.netapp.dataontap import block_base +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp import utils as na_utils from cinder.volume import utils as volume_utils @@ -43,9 +42,6 @@ class NetAppBlockStorageLibraryTestCase(test.TestCase): def setUp(self): super(NetAppBlockStorageLibraryTestCase, self).setUp() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([block_base]) - kwargs = {'configuration': self.get_config_base()} self.library = block_base.NetAppBlockStorageLibrary( 'driver', 'protocol', **kwargs) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py index ed26b7535bd..c2bba088844 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py @@ -23,12 +23,11 @@ from oslo_service import loopingcall from cinder import exception from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) import cinder.tests.unit.volume.drivers.netapp.dataontap.fakes as fake import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes from cinder.volume.drivers.netapp.dataontap import block_base from cinder.volume.drivers.netapp.dataontap import block_cmode +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.drivers.netapp.dataontap import ssc_cmode from cinder.volume.drivers.netapp import utils as na_utils @@ -41,9 +40,6 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): def setUp(self): super(NetAppBlockStorageCmodeLibraryTestCase, self).setUp() - # Inject fake netapp_lib module classes. - netapp_api.mock_netapp_lib([block_cmode]) - kwargs = {'configuration': self.get_config_cmode()} self.library = block_cmode.NetAppBlockStorageCmodeLibrary( 'driver', 'protocol', **kwargs) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py index 0614b83a886..c706e9cf0c6 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py @@ -24,11 +24,10 @@ from oslo_utils import units from cinder import exception from cinder import test -from cinder.tests.unit.volume.drivers.netapp.dataontap.client import ( - fake_api as netapp_api) from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake from cinder.tests.unit.volume.drivers.netapp import fakes as na_fakes from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_cmode from cinder.volume.drivers.netapp.dataontap import nfs_base from cinder.volume.drivers.netapp.dataontap import nfs_cmode diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py index d582f1f5802..68b81fd8a7d 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/fakes.py @@ -26,13 +26,6 @@ from cinder.volume.drivers.netapp.eseries import utils import cinder.volume.drivers.netapp.options as na_opts -def mock_netapp_lib(modules): - """Inject fake netapp_lib module classes.""" - netapp_lib = mock.Mock() - netapp_lib.api.rest.rest.WebserviceClient = mock.Mock() - for module in modules: - setattr(module, 'netapp_restclient', netapp_lib.api.rest.rest) - MULTIATTACH_HOST_GROUP = { 'clusterRef': '8500000060080E500023C7340036035F515B78FC', 'label': utils.MULTI_ATTACH_HOST_GROUP_NAME, @@ -855,6 +848,8 @@ FAKE_ENDPOINT_HTTP = 'http://host:80/endpoint' FAKE_ENDPOINT_HTTPS = 'https://host:8443/endpoint' +FAKE_INVOC_MSG = 'success' + FAKE_CLIENT_PARAMS = { 'scheme': 'http', 'host': '127.0.0.1', diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py index 686166c0cbd..dcd9fca5e78 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py @@ -41,13 +41,10 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): self.mock_object(client, 'LOG', self.mock_log) self.fake_password = 'mysecret' - # Inject fake netapp_lib module classes. - eseries_fake.mock_netapp_lib([client]) - self.my_client = client.RestClient('http', 'host', '80', '/test', 'user', self.fake_password, system_id='fake_sys_id') - self.my_client.client._endpoint = eseries_fake.FAKE_ENDPOINT_HTTP + self.my_client._endpoint = eseries_fake.FAKE_ENDPOINT_HTTP fake_response = mock.Mock() fake_response.status_code = 200 @@ -67,8 +64,8 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): fake_resp.status_code = status_code expected_msg = "Response error code - %s." % status_code - with self.assertRaisesRegexp(es_exception.WebServiceException, - expected_msg) as exc: + with self.assertRaisesRegex(es_exception.WebServiceException, + expected_msg) as exc: self.my_client._eval_response(fake_resp) self.assertEqual(status_code, exc.status_code) @@ -81,8 +78,8 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): fake_resp.text = resp_text expected_msg = "Response error - %s." % resp_text - with self.assertRaisesRegexp(es_exception.WebServiceException, - expected_msg) as exc: + with self.assertRaisesRegex(es_exception.WebServiceException, + expected_msg) as exc: self.my_client._eval_response(fake_resp) self.assertEqual(status_code, exc.status_code) @@ -413,7 +410,7 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): client.RestClient, '_get_resource_url', mock.Mock(return_value=eseries_fake.FAKE_RESOURCE_URL)) self.mock_object( - self.my_client.client, 'invoke_service', + self.my_client, 'invoke_service', mock.Mock(return_value=fake_invoke_service)) eseries_info = client.RestClient.get_eseries_api_info( @@ -732,3 +729,42 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): client.RestClient._init_features(self.my_client) self.assertTrue(self.my_client.features.SSC_API_V2.supported) + + +@ddt.ddt +class TestWebserviceClientTestCase(test.TestCase): + + def setUp(self): + """sets up the mock tests""" + super(TestWebserviceClientTestCase, self).setUp() + self.mock_log = mock.Mock() + self.mock_object(client, 'LOG', self.mock_log) + self.webclient = client.WebserviceClient('http', 'host', '80', + '/test', 'user', '****') + + @ddt.data({'params': {'host': None, 'scheme': 'https', 'port': '80'}}, + {'params': {'host': 'host', 'scheme': None, 'port': '80'}}, + {'params': {'host': 'host', 'scheme': 'http', 'port': None}}) + @ddt.unpack + def test__validate_params_value_error(self, params): + """Tests various scenarios for ValueError in validate method""" + self.assertRaises(exception.InvalidInput, + self.webclient._validate_params, **params) + + def test_invoke_service_no_endpoint_error(self): + """Tests Exception and Log error if no endpoint is provided""" + self.webclient._endpoint = None + log_error = 'Unexpected error while invoking web service' + + self.assertRaises(exception.NetAppDriverException, + self.webclient.invoke_service) + self.assertTrue(self.mock_log.exception.find(log_error)) + + def test_invoke_service(self): + """Tests if invoke_service evaluates the right response""" + self.webclient._endpoint = eseries_fake.FAKE_ENDPOINT_HTTP + self.mock_object(self.webclient.conn, 'request', + mock.Mock(return_value=eseries_fake.FAKE_INVOC_MSG)) + result = self.webclient.invoke_service() + + self.assertIsNotNone(result) diff --git a/cinder/tests/unit/volume/drivers/netapp/test_common.py b/cinder/tests/unit/volume/drivers/netapp/test_common.py index 85f112a6170..db3a6362743 100644 --- a/cinder/tests/unit/volume/drivers/netapp/test_common.py +++ b/cinder/tests/unit/volume/drivers/netapp/test_common.py @@ -35,7 +35,6 @@ class NetAppDriverFactoryTestCase(test.TestCase): mock.Mock(return_value='fake_info')) 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' @@ -47,7 +46,6 @@ class NetAppDriverFactoryTestCase(test.TestCase): kwargs['app_version'] = 'fake_info' mock_create_driver.assert_called_with('fake_family', 'fake_protocol', *(), **kwargs) - mock_check_netapp_lib.assert_called_once_with() def test_new_missing_config(self): diff --git a/cinder/tests/unit/volume/drivers/netapp/test_utils.py b/cinder/tests/unit/volume/drivers/netapp/test_utils.py index 4cdd32791a6..28c288a8ff0 100644 --- a/cinder/tests/unit/volume/drivers/netapp/test_utils.py +++ b/cinder/tests/unit/volume/drivers/netapp/test_utils.py @@ -23,7 +23,6 @@ import platform import mock from oslo_concurrency import processutils as putils -from oslo_utils import importutils from cinder import context from cinder import exception @@ -64,21 +63,6 @@ class NetAppDriverUtilsTestCase(test.TestCase): setattr(configuration, 'flag2', 'value2') self.assertIsNone(na_utils.check_flags(required_flags, configuration)) - 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.NetAppDriverException, - na_utils.check_netapp_lib) - def test_to_bool(self): self.assertTrue(na_utils.to_bool(True)) self.assertTrue(na_utils.to_bool('true')) diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index 7c2a200fab0..8414096ad8e 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -73,7 +73,6 @@ class NetAppDriver(driver.ProxyVD): 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: %(info)s'), diff --git a/cinder/volume/drivers/netapp/dataontap/block_base.py b/cinder/volume/drivers/netapp/dataontap/block_base.py index d04cdd5f572..8db15678dca 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_base.py +++ b/cinder/volume/drivers/netapp/dataontap/block_base.py @@ -29,22 +29,18 @@ import uuid from oslo_log import log as logging from oslo_log import versionutils from oslo_utils import excutils -from oslo_utils import importutils from oslo_utils import units import six from cinder import exception from cinder.i18n import _, _LE, _LI, _LW from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp import options as na_opts from cinder.volume.drivers.netapp import utils as na_utils from cinder.volume import utils as volume_utils from cinder.zonemanager import utils as fczm_utils -netapp_lib = importutils.try_import('netapp_lib') -if netapp_lib: - from netapp_lib.api.zapi import zapi as netapp_api - LOG = logging.getLogger(__name__) diff --git a/cinder/volume/drivers/netapp/dataontap/client/api.py b/cinder/volume/drivers/netapp/dataontap/client/api.py new file mode 100644 index 00000000000..b3a58c2b551 --- /dev/null +++ b/cinder/volume/drivers/netapp/dataontap/client/api.py @@ -0,0 +1,613 @@ +# Copyright (c) 2012 NetApp, Inc. All rights reserved. +# Copyright (c) 2014 Navneet Singh. All rights reserved. +# Copyright (c) 2014 Glenn Gobeli. All rights reserved. +# Copyright (c) 2014 Clinton Knight. All rights reserved. +# Copyright (c) 2015 Alex Meade. 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 as logging +import six +from six.moves import urllib + +from cinder import exception +from cinder.i18n import _ + +LOG = logging.getLogger(__name__) + +ESIS_CLONE_NOT_LICENSED = '14956' + + +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): + 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._refresh_conn = True + + 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 not transport_type: + raise ValueError('No transport type specified') + 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 invoke_elem(self, na_element, enable_tunneling=False): + """Invoke the API on the server.""" + if not na_element or not isinstance(na_element, NaElement): + raise ValueError('NaElement must be supplied to invoke API') + + request, request_element = self._create_request(na_element, + enable_tunneling) + + 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: + raise NaApiError('Unexpected error') + + response_xml = response.read() + response_element = self._get_result(response_xml) + + 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 __str__(self): + return self.to_string(pretty=True) + + def __repr__(self): + return str(self) + + 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. + + 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) + + +NaErrors = {'API_NOT_FOUND': NaApiError('13005', 'Unable to find API'), + 'INSUFFICIENT_PRIVS': NaApiError('13003', + 'Insufficient privileges')} + + +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/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py index 3a1cdd430e5..55d2471c7b5 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_7mode.py @@ -19,18 +19,14 @@ import math import time from oslo_log import log as logging -from oslo_utils import importutils import six from cinder import exception from cinder.i18n import _, _LW from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base -netapp_lib = importutils.try_import('netapp_lib') -if netapp_lib: - from netapp_lib.api.zapi import zapi as netapp_api - LOG = logging.getLogger(__name__) diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_base.py b/cinder/volume/drivers/netapp/dataontap/client/client_base.py index b37353dea9f..1e9e2c64fcc 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_base.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_base.py @@ -20,19 +20,15 @@ import sys from oslo_log import log as logging from oslo_utils import excutils -from oslo_utils import importutils from oslo_utils import timeutils import six from cinder.i18n import _LE, _LW, _LI from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.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 = logging.getLogger(__name__) diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py index f631dd4da6f..7a726b0b2b1 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py @@ -19,20 +19,15 @@ import copy import math from oslo_log import log as logging -from oslo_utils import importutils import six from cinder import exception from cinder.i18n import _, _LW from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base from cinder.volume.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 = logging.getLogger(__name__) DELETED_PREFIX = 'deleted_cinder_' @@ -528,8 +523,10 @@ class Client(client_base.Client): self.connection.invoke_successfully(na_el) except Exception as e: if isinstance(e, netapp_api.NaApiError): - if(e.code == netapp_error.EAPINOTFOUND - or e.code == netapp_error.EAPIPRIVILEGE): + if (e.code == netapp_api.NaErrors + ['API_NOT_FOUND'].code or + e.code == netapp_api.NaErrors + ['INSUFFICIENT_PRIVS'].code): failed_apis.append(api_name) elif major == 1 and minor >= 20: failed_apis = copy.copy(api_list) diff --git a/cinder/volume/drivers/netapp/dataontap/ssc_cmode.py b/cinder/volume/drivers/netapp/dataontap/ssc_cmode.py index 411fcce22fe..8a9a321f1ae 100644 --- a/cinder/volume/drivers/netapp/dataontap/ssc_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/ssc_cmode.py @@ -24,19 +24,15 @@ import copy import threading from oslo_log import log as logging -from oslo_utils import importutils from oslo_utils import timeutils import six from cinder import exception from cinder.i18n import _, _LI, _LW from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.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 = logging.getLogger(__name__) diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py index 7b96f02ad11..a97ced89d56 100644 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ b/cinder/volume/drivers/netapp/eseries/client.py @@ -26,26 +26,72 @@ import json import uuid from oslo_log import log as logging -from oslo_utils import importutils +import requests import six from six.moves import urllib from cinder import exception from cinder.i18n import _ +from cinder.i18n import _LE import cinder.utils as cinder_utils from cinder.volume.drivers.netapp.eseries import exception as es_exception from cinder.volume.drivers.netapp.eseries import utils from cinder.volume.drivers.netapp import utils as na_utils -netapp_lib = importutils.try_import('netapp_lib') -if netapp_lib: - from netapp_lib.api.rest import rest as netapp_restclient - LOG = logging.getLogger(__name__) -class RestClient(object): +class WebserviceClient(object): + """Base client for NetApp Storage web services.""" + + def __init__(self, scheme, host, port, service_path, username, + password, **kwargs): + self._validate_params(scheme, host, port) + self._create_endpoint(scheme, host, port, service_path) + self._username = username + self._password = password + self._init_connection() + + def _validate_params(self, scheme, host, port): + """Does some basic validation for web service params.""" + if host is None or port is None or scheme is None: + msg = _('One of the required inputs from host, ' + 'port or scheme was not found.') + raise exception.InvalidInput(reason=msg) + if scheme not in ('http', 'https'): + raise exception.InvalidInput(reason=_("Invalid transport type.")) + + def _create_endpoint(self, scheme, host, port, service_path): + """Creates end point url for the service.""" + netloc = '%s:%s' % (host, port) + self._endpoint = urllib.parse.urlunparse((scheme, netloc, service_path, + None, None, None)) + + def _init_connection(self): + """Do client specific set up for session and connection pooling.""" + self.conn = requests.Session() + if self._username and self._password: + self.conn.auth = (self._username, self._password) + + def invoke_service(self, method='GET', url=None, params=None, data=None, + headers=None, timeout=None, verify=False): + url = url or self._endpoint + try: + response = self.conn.request(method, url, params, data, + headers=headers, timeout=timeout, + verify=verify) + # Catching error conditions other than the perceived ones. + # Helps propagating only known exceptions back to the caller. + except Exception as e: + LOG.exception(_LE("Unexpected error while invoking web service." + " Error - %s."), e) + raise exception.NetAppDriverException( + _("Invoking web service failed.")) + return response + + +class RestClient(WebserviceClient): """REST client specific to e-series storage service.""" ID = 'id' @@ -66,11 +112,11 @@ class RestClient(object): def __init__(self, scheme, host, port, service_path, username, password, **kwargs): + super(RestClient, self).__init__(scheme, host, port, service_path, + username, password, **kwargs) + kwargs = kwargs or {} - self.client = netapp_restclient.WebserviceClient(scheme, host, port, - service_path, - username, password, - **kwargs) + self._system_id = kwargs.get('system_id') self._content_type = kwargs.get('content_type') or 'json' @@ -149,9 +195,9 @@ class RestClient(object): raise exception.NotFound(_('Storage system id not set.')) kwargs['system-id'] = self._system_id path = path.format(**kwargs) - if not self.client._endpoint.endswith('/'): - self.client._endpoint = '%s/' % self.client._endpoint - return urllib.parse.urljoin(self.client._endpoint, path.lstrip('/')) + if not self._endpoint.endswith('/'): + self._endpoint = '%s/' % self._endpoint + return urllib.parse.urljoin(self._endpoint, path.lstrip('/')) def _invoke(self, method, path, data=None, use_system=True, timeout=None, verify=False, **kwargs): @@ -163,9 +209,9 @@ class RestClient(object): if cinder_utils.TRACE_API: self._log_http_request(method, url, headers, data) data = json.dumps(data) if data else None - res = self.client.invoke_service(method, url, data=data, - headers=headers, - timeout=timeout, verify=verify) + res = self.invoke_service(method, url, data=data, + headers=headers, + timeout=timeout, verify=verify) res_dict = res.json() if res.text else None if cinder_utils.TRACE_API: @@ -664,9 +710,9 @@ class RestClient(object): 'Accept': 'application/json'} url = self._get_resource_url(path, True).replace( '/devmgr/v2', '', 1) - result = self.client.invoke_service(method='GET', url=url, - headers=headers, - verify=verify) + result = self.invoke_service(method='GET', url=url, + headers=headers, + verify=verify) about_response_dict = result.json() mode_is_proxy = about_response_dict['runningAsProxy'] if mode_is_proxy: diff --git a/cinder/volume/drivers/netapp/utils.py b/cinder/volume/drivers/netapp/utils.py index b6a13b04d66..0dbb1016c6f 100644 --- a/cinder/volume/drivers/netapp/utils.py +++ b/cinder/volume/drivers/netapp/utils.py @@ -29,7 +29,6 @@ import socket from oslo_concurrency import processutils as putils from oslo_log import log as logging -from oslo_utils import importutils import six from cinder import context @@ -76,14 +75,6 @@ def check_flags(required_flags, configuration): raise exception.InvalidInput(reason=msg) -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.NetAppDriverException(msg) - - def to_bool(val): """Converts true, yes, y, 1 to True, False otherwise.""" if val: