From 1e8185cde4aecf0b06e16028b1c3e8a4ee671e01 Mon Sep 17 00:00:00 2001 From: Yuriy Zveryanskyy Date: Thu, 20 Aug 2015 17:26:46 +0300 Subject: [PATCH] Add vendor interface to ipminative driver This patch adds vendor interface with send_raw() and bmc_reset() methods to ipminative driver (like in ipmitool). Change-Id: I388a97730c627f5184959b6dac64795d61ef140e --- ironic/drivers/fake.py | 1 + ironic/drivers/modules/ipminative.py | 100 ++++++++++++++++++++++++ ironic/tests/drivers/test_ipminative.py | 84 ++++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py index 5b8514fb83..657cee0ef8 100644 --- a/ironic/drivers/fake.py +++ b/ironic/drivers/fake.py @@ -112,6 +112,7 @@ class FakeIPMINativeDriver(base.BaseDriver): self.power = ipminative.NativeIPMIPower() self.console = ipminative.NativeIPMIShellinaboxConsole() self.deploy = fake.FakeDeploy() + self.vendor = ipminative.VendorPassthru() self.management = ipminative.NativeIPMIManagement() diff --git a/ironic/drivers/modules/ipminative.py b/ironic/drivers/modules/ipminative.py index d9f7b2f51f..f80f997afd 100644 --- a/ironic/drivers/modules/ipminative.py +++ b/ironic/drivers/modules/ipminative.py @@ -297,6 +297,44 @@ def _get_sensors_data(driver_info): return sensors_data +def _parse_raw_bytes(raw_bytes): + """Parse raw bytes string. + + :param raw_bytes: a string of hexadecimal raw bytes, e.g. '0x00 0x01'. + :returns: a tuple containing the arguments for pyghmi call as integers, + (IPMI net function, IPMI command, list of command's data). + :raises: InvalidParameterValue when an invalid value is specified. + """ + try: + bytes_list = [int(x, base=16) for x in raw_bytes.split()] + return bytes_list[0], bytes_list[1], bytes_list[2:] + except ValueError: + raise exception.InvalidParameterValue(_( + "Invalid raw bytes string: '%s'") % raw_bytes) + except IndexError: + raise exception.InvalidParameterValue(_( + "Raw bytes string requires two bytes at least.")) + + +def _send_raw(driver_info, raw_bytes): + """Send raw bytes to the BMC.""" + netfn, command, data = _parse_raw_bytes(raw_bytes) + LOG.debug("Sending raw bytes %(bytes)s to node %(node_id)s", + {'bytes': raw_bytes, 'node_id': driver_info['uuid']}) + try: + ipmicmd = ipmi_command.Command(bmc=driver_info['address'], + userid=driver_info['username'], + password=driver_info['password']) + ipmicmd.xraw_command(netfn, command, data=data) + except pyghmi_exception.IpmiException as e: + msg = (_("IPMI send raw bytes '%(bytes)s' failed for node %(node_id)s" + " with the following error: %(error)s") % + {'bytes': raw_bytes, 'node_id': driver_info['uuid'], + 'error': e}) + LOG.error(msg) + raise exception.IPMIFailure(msg) + + class NativeIPMIPower(base.PowerInterface): """The power driver using native python-ipmi library.""" @@ -569,3 +607,65 @@ class NativeIPMIShellinaboxConsole(base.ConsoleInterface): driver_info = _parse_driver_info(task.node) url = console_utils.get_shellinabox_console_url(driver_info['port']) return {'type': 'shellinabox', 'url': url} + + +class VendorPassthru(base.VendorInterface): + + def get_properties(self): + return COMMON_PROPERTIES + + def validate(self, task, method, **kwargs): + """Validate vendor-specific actions. + + :param task: a task from TaskManager. + :param method: method to be validated + :param kwargs: info for action. + :raises: InvalidParameterValue when an invalid parameter value is + specified. + :raises: MissingParameterValue if a required parameter is missing. + + """ + if method == 'send_raw': + raw_bytes = kwargs.get('raw_bytes') + if not raw_bytes: + raise exception.MissingParameterValue(_( + 'Parameter raw_bytes (string of bytes) was not ' + 'specified.')) + _parse_raw_bytes(raw_bytes) + + _parse_driver_info(task.node) + + @base.passthru(['POST']) + @task_manager.require_exclusive_lock + def send_raw(self, task, http_method, raw_bytes): + """Send raw bytes to the BMC. Bytes should be a string of bytes. + + :param task: a TaskManager instance. + :param http_method: the HTTP method used on the request. + :param raw_bytes: a string of raw bytes to send, e.g. '0x00 0x01' + :raises: IPMIFailure on an error from native IPMI call. + :raises: MissingParameterValue if a required parameter is missing. + :raises: InvalidParameterValue when an invalid value is specified. + + """ + driver_info = _parse_driver_info(task.node) + _send_raw(driver_info, raw_bytes) + + @base.passthru(['POST']) + @task_manager.require_exclusive_lock + def bmc_reset(self, task, http_method, warm=True): + """Reset BMC via IPMI command. + + :param task: a TaskManager instance. + :param http_method: the HTTP method used on the request. + :param warm: boolean parameter to decide on warm or cold reset. + :raises: IPMIFailure on an error from native IPMI call. + :raises: MissingParameterValue if a required parameter is missing. + :raises: InvalidParameterValue when an invalid value is specified + + """ + driver_info = _parse_driver_info(task.node) + # NOTE(yuriyz): pyghmi 0.8.0 does not have a method for BMC reset + command = '0x03' if warm else '0x02' + raw_command = '0x06 ' + command + _send_raw(driver_info, raw_command) diff --git a/ironic/tests/drivers/test_ipminative.py b/ironic/tests/drivers/test_ipminative.py index f2d18919f4..06db4413fe 100644 --- a/ironic/tests/drivers/test_ipminative.py +++ b/ironic/tests/drivers/test_ipminative.py @@ -201,6 +201,38 @@ class IPMINativePrivateMethodTestCase(db_base.DbTestCase): ret = ipminative._get_sensors_data(self.info) self.assertEqual(expected, ret) + def test__parse_raw_bytes_ok(self): + bytes_string = '0x11 0x12 0x25 0xFF' + netfn, cmd, data = ipminative._parse_raw_bytes(bytes_string) + self.assertEqual(0x11, netfn) + self.assertEqual(0x12, cmd) + self.assertEqual([0x25, 0xFF], data) + + def test__parse_raw_bytes_invalid_value(self): + bytes_string = '0x11 oops' + self.assertRaises(exception.InvalidParameterValue, + ipminative._parse_raw_bytes, + bytes_string) + + def test__parse_raw_bytes_missing_byte(self): + bytes_string = '0x11' + self.assertRaises(exception.InvalidParameterValue, + ipminative._parse_raw_bytes, + bytes_string) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__send_raw(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipminative._send_raw(self.info, '0x01 0x02 0x03 0x04') + ipmicmd.xraw_command.assert_called_once_with(1, 2, data=[3, 4]) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__send_raw_fail(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.xraw_command.side_effect = pyghmi_exception.IpmiException() + self.assertRaises(exception.IPMIFailure, ipminative._send_raw, + self.info, '0x01 0x02') + class IPMINativeDriverTestCase(db_base.DbTestCase): """Test cases for ipminative.NativeIPMIPower class functions.""" @@ -219,6 +251,7 @@ class IPMINativeDriverTestCase(db_base.DbTestCase): expected = ipminative.COMMON_PROPERTIES self.assertEqual(expected, self.driver.power.get_properties()) self.assertEqual(expected, self.driver.management.get_properties()) + self.assertEqual(expected, self.driver.vendor.get_properties()) expected = list(ipminative.COMMON_PROPERTIES) expected += list(ipminative.CONSOLE_PROPERTIES) @@ -466,3 +499,54 @@ class IPMINativeDriverTestCase(db_base.DbTestCase): self.assertEqual(expected, console_info) mock_exec.assert_called_once_with(self.info['port']) self.assertTrue(mock_exec.called) + + @mock.patch.object(ipminative, '_parse_driver_info', autospec=True) + @mock.patch.object(ipminative, '_parse_raw_bytes', autospec=True) + def test_vendor_passthru_validate__send_raw_bytes_good(self, mock_raw, + mock_driver): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.driver.vendor.validate(task, + method='send_raw', + http_method='POST', + raw_bytes='0x00 0x01') + mock_raw.assert_called_once_with('0x00 0x01') + mock_driver.assert_called_once_with(task.node) + + def test_vendor_passthru_validate__send_raw_bytes_fail(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.MissingParameterValue, + self.driver.vendor.validate, + task, method='send_raw') + + def test_vendor_passthru_vendor_routes(self): + expected = ['send_raw', 'bmc_reset'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + vendor_routes = task.driver.vendor.vendor_routes + self.assertIsInstance(vendor_routes, dict) + self.assertEqual(sorted(expected), sorted(vendor_routes)) + + @mock.patch.object(ipminative, '_send_raw', autospec=True) + def test_send_raw(self, send_raw_mock): + bytes = '0x00 0x01' + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.vendor.send_raw(task, http_method='POST', + raw_bytes=bytes) + + send_raw_mock.assert_called_once_with(self.info, bytes) + + @mock.patch.object(ipminative, '_send_raw', autospec=True) + def _test_bmc_reset(self, warm, send_raw_mock): + expected_bytes = '0x06 0x03' if warm else '0x06 0x02' + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.vendor.bmc_reset(task, http_method='POST', warm=warm) + + send_raw_mock.assert_called_once_with(self.info, expected_bytes) + + def test_bmc_reset_cold(self): + self._test_bmc_reset(False) + + def test_bmc_reset_warm(self): + self._test_bmc_reset(True)