From 97a8ae1c7765aae06b86162b3191186b14f1c8c6 Mon Sep 17 00:00:00 2001 From: Hugo Nicodemos Date: Wed, 29 Nov 2017 14:26:13 -0300 Subject: [PATCH] Introduce hpOneView and ilorest to OneView It introduces the ``hpOneView`` and ``ilorest`` library to the OneView Driver. This patch will be used as the standard patch to other patches related to the removal of the ``python-oneviewclient`` library dependency. Change-Id: Ib9d72ff5713d58631bcdccc817707b5d512e156e Partial-Bug: #1693788 Co-Authored-By: Fellype Cavalcante Co-Authored-By: Ricardo Araujo --- driver-requirements.txt | 2 + ironic/drivers/modules/oneview/common.py | 92 +++++++++++++++++++ .../drivers/modules/oneview/test_common.py | 47 ++++++++++ .../drivers/modules/oneview/test_inspect.py | 2 + .../drivers/third_party_driver_mock_specs.py | 18 ++++ .../unit/drivers/third_party_driver_mocks.py | 17 ++++ 6 files changed, 178 insertions(+) diff --git a/driver-requirements.txt b/driver-requirements.txt index 952f3a4e77..ea09434315 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -9,6 +9,8 @@ pysnmp python-ironic-inspector-client>=1.5.0 python-oneviewclient<3.0.0,>=2.5.2 python-scciclient>=0.6.0 +python-ilorest-library>=2.1.0 +hpOneView>=4.4.0 UcsSdk==0.8.2.2 python-dracclient>=1.3.0 diff --git a/ironic/drivers/modules/oneview/common.py b/ironic/drivers/modules/oneview/common.py index ca80782d83..2e7e1ab77f 100644 --- a/ironic/drivers/modules/oneview/common.py +++ b/ironic/drivers/modules/oneview/common.py @@ -13,8 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. +import re + from oslo_log import log as logging from oslo_utils import importutils +from six.moves.urllib import parse from ironic.common import exception from ironic.common.i18n import _ @@ -29,6 +32,9 @@ oneview_utils = importutils.try_import('oneview_client.utils') oneview_states = importutils.try_import('oneview_client.states') oneview_exceptions = importutils.try_import('oneview_client.exceptions') +hponeview_client = importutils.try_import('hpOneView.oneview_client') +redfish = importutils.try_import('redfish') + REQUIRED_ON_DRIVER_INFO = { 'server_hardware_uri': _("Server Hardware URI. Required in driver_info."), } @@ -48,6 +54,8 @@ OPTIONAL_ON_PROPERTIES = { "Enclosure Group URI. Optional in properties/capabilities."), } +ILOREST_BASE_PORT = "443" + COMMON_PROPERTIES = {} COMMON_PROPERTIES.update(REQUIRED_ON_DRIVER_INFO) COMMON_PROPERTIES.update(REQUIRED_ON_PROPERTIES) @@ -82,6 +90,90 @@ def get_oneview_client(): return oneview_client +def prepare_manager_url(manager_url): + # NOTE(mrtenio) python-oneviewclient uses https or http in the manager_url + # while python-hpOneView does not. This will not be necessary when + # python-hpOneView client is the only OneView library. + if manager_url: + url_match = "^(http[s]?://)?([^/]+)(/.*)?$" + manager_url = re.search(url_match, manager_url).group(2) + return manager_url + + +def get_hponeview_client(): + """Generate an instance of the hpOneView client. + + Generates an instance of the hpOneView client using the hpOneView library. + + :returns: an instance of the OneViewClient + :raises: InvalidParameterValue if mandatory information is missing on the + node or on invalid input. + :raises: OneViewError if try a secure connection without CA certificate. + """ + manager_url = prepare_manager_url(CONF.oneview.manager_url) + + insecure = CONF.oneview.allow_insecure_connections + ssl_certificate = CONF.oneview.tls_cacert_file + + if not (insecure or ssl_certificate): + msg = _("TLS CA certificate to connect with OneView is missing.") + raise exception.OneViewError(error=msg) + + # NOTE(nicodemos) Ignore the CA certificate if it's an insecure connection + if insecure and ssl_certificate: + LOG.debug("Doing an insecure connection with OneView, the CA " + "certificate file: %s will be ignored.", ssl_certificate) + ssl_certificate = None + + config = { + "ip": manager_url, + "credentials": { + "userName": CONF.oneview.username, + "password": CONF.oneview.password + }, + "ssl_certificate": ssl_certificate + } + return hponeview_client.OneViewClient(config) + + +def get_ilorest_client(server_hardware): + """Generate an instance of the iLORest library client. + + :param: server_hardware: a server hardware uuid or uri + :returns: an instance of the iLORest client + :raises: InvalidParameterValue if mandatory information is missing on the + node or on invalid input. + """ + oneview_client = get_hponeview_client() + remote_console = oneview_client.server_hardware.get_remote_console_url( + server_hardware + ) + host_ip, ilo_token = _get_ilo_access(remote_console) + base_url = "https://%s:%s" % (host_ip, ILOREST_BASE_PORT) + return redfish.rest_client(base_url=base_url, sessionkey=ilo_token) + + +def _get_ilo_access(remote_console): + """Get the needed information to access ilo. + + Get the host_ip and a token of an iLO remote console instance which can be + used to perform operations on that controller. + + The Remote Console url has the following format: + hplocons://addr=1.2.3.4&sessionkey=a79659e3b3b7c8209c901ac3509a6719 + + :param: remote_console: OneView Remote Console object with a + remoteConsoleUrl + :returns: A tuple with the Host IP and Token to access ilo, for + example: ('1.2.3.4', 'a79659e3b3b7c8209c901ac3509a6719') + """ + url = remote_console.get('remoteConsoleUrl') + url_parse = parse.urlparse(url) + host_ip = parse.parse_qs(url_parse.netloc).get('addr')[0] + token = parse.parse_qs(url_parse.netloc).get('sessionkey')[0] + return host_ip, token + + def verify_node_info(node): """Verifies if fields and namespaces of a node are valid. diff --git a/ironic/tests/unit/drivers/modules/oneview/test_common.py b/ironic/tests/unit/drivers/modules/oneview/test_common.py index 47d16c42f9..56e29395c3 100644 --- a/ironic/tests/unit/drivers/modules/oneview/test_common.py +++ b/ironic/tests/unit/drivers/modules/oneview/test_common.py @@ -25,6 +25,7 @@ from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import utils as db_utils from ironic.tests.unit.objects import utils as obj_utils +hponeview_client = importutils.try_import('hpOneView.oneview_client') oneview_states = importutils.try_import('oneview_client.states') @@ -40,8 +41,54 @@ class OneViewCommonTestCase(db_base.DbTestCase): self.config(manager_url='https://1.2.3.4', group='oneview') self.config(username='user', group='oneview') self.config(password='password', group='oneview') + self.config(tls_cacert_file='ca_file', group='oneview') + self.config(allow_insecure_connections=False, group='oneview') mgr_utils.mock_the_extension_manager(driver="fake_oneview") + def test_prepare_manager_url(self): + self.assertEqual( + common.prepare_manager_url("https://1.2.3.4/"), "1.2.3.4") + self.assertEqual( + common.prepare_manager_url("http://oneview"), "oneview") + self.assertEqual( + common.prepare_manager_url("http://oneview:8080"), "oneview:8080") + self.assertEqual( + common.prepare_manager_url("http://oneview/something"), "oneview") + self.assertEqual( + common.prepare_manager_url("oneview/something"), "oneview") + self.assertEqual( + common.prepare_manager_url("oneview"), "oneview") + + @mock.patch.object(hponeview_client, 'OneViewClient', autospec=True) + def test_get_hponeview_client(self, mock_hponeview_client): + common.get_hponeview_client() + mock_hponeview_client.assert_called_once_with(self.config) + + def test_get_hponeview_client_insecure_false(self): + self.config(tls_cacert_file=None, group='oneview') + self.assertRaises(exception.OneViewError, common.get_hponeview_client) + + @mock.patch.object(hponeview_client, 'OneViewClient', autospec=True) + def test_get_hponeview_client_insecure_cafile(self, mock_oneview): + self.config(allow_insecure_connections=True, group='oneview') + credentials = { + "ip": 'https://1.2.3.4', + "credentials": { + "userName": 'user', + "password": 'password' + }, + "ssl_certificate": None + } + mock_oneview.assert_called_once_with(credentials) + + def test_get_ilo_access(self): + url = ("hplocons://addr=1.2.3.4&sessionkey" + + "=a79659e3b3b7c8209c901ac3509a6719") + remote_console = {'remoteConsoleUrl': url} + host_ip, token = common._get_ilo_access(remote_console) + self.assertEqual(host_ip, "1.2.3.4") + self.assertEqual(token, "a79659e3b3b7c8209c901ac3509a6719") + def test_verify_node_info(self): common.verify_node_info(self.node) diff --git a/ironic/tests/unit/drivers/modules/oneview/test_inspect.py b/ironic/tests/unit/drivers/modules/oneview/test_inspect.py index b2ef499a03..ebc01b1013 100644 --- a/ironic/tests/unit/drivers/modules/oneview/test_inspect.py +++ b/ironic/tests/unit/drivers/modules/oneview/test_inspect.py @@ -31,6 +31,7 @@ class AgentPXEOneViewInspectTestCase(db_base.DbTestCase): def setUp(self): super(AgentPXEOneViewInspectTestCase, self).setUp() self.config(enabled=True, group='inspector') + self.config(manager_url='https://1.2.3.4', group='oneview') mgr_utils.mock_the_extension_manager(driver="agent_pxe_oneview") self.node = obj_utils.create_test_node( self.context, driver='agent_pxe_oneview', @@ -69,6 +70,7 @@ class ISCSIPXEOneViewInspectTestCase(db_base.DbTestCase): def setUp(self): super(ISCSIPXEOneViewInspectTestCase, self).setUp() self.config(enabled=True, group='inspector') + self.config(manager_url='https://1.2.3.4', group='oneview') mgr_utils.mock_the_extension_manager(driver="iscsi_pxe_oneview") self.node = obj_utils.create_test_node( self.context, driver='iscsi_pxe_oneview', diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index f21adafd46..d3c006b140 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -144,6 +144,24 @@ ONEVIEWCLIENT_STATES_SPEC = ( 'ONEVIEW_ERROR', ) +HPE_ONEVIEW_SPEC = ( + 'oneview_client', + 'resources', + 'exceptions', +) + +HPE_ONEVIEW_CLS_SPEC = ( +) + +HPE_ONEVIEW_STATES_SPEC = ( + 'ONEVIEW_POWER_OFF', + 'ONEVIEW_POWERING_OFF', + 'ONEVIEW_POWER_ON', + 'ONEVIEW_POWERING_ON', + 'ONEVIEW_RESETTING', + 'ONEVIEW_ERROR', +) + SUSHY_CONSTANTS_SPEC = ( 'BOOT_SOURCE_TARGET_PXE', 'BOOT_SOURCE_TARGET_HDD', diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index 31fec78b75..430293bb6e 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -26,6 +26,7 @@ Current list of mocked libraries: - pysnmp - scciclient - oneview_client +- hpOneView - pywsman - python-dracclient """ @@ -99,6 +100,22 @@ if 'ironic.drivers.oneview' in sys.modules: six.moves.reload_module(sys.modules['ironic.drivers.modules.oneview']) +hpOneView = importutils.try_import('hpOneView') +if not hpOneView: + hpOneView = mock.MagicMock(spec_set=mock_specs.HPE_ONEVIEW_SPEC) + sys.modules['hpOneView'] = hpOneView + sys.modules['hpOneView.oneview_client'] = hpOneView.oneview_client + sys.modules['hpOneView.resources'] = hpOneView.resources + sys.modules['hpOneView.exceptions'] = hpOneView.exceptions + hpOneView.exceptions.HPOneViewException = type('HPOneViewException', + (Exception,), {}) +sys.modules['hpOneView.oneview_client'].OneViewClient = mock.MagicMock( + spec_set=mock_specs.HPE_ONEVIEW_CLS_SPEC +) +if 'ironic.drivers.oneview' in sys.modules: + six.moves.reload_module(sys.modules['ironic.drivers.modules.oneview']) + + # attempt to load the external 'python-dracclient' library, which is required # by the optional drivers.modules.drac module dracclient = importutils.try_import('dracclient')