diff options
Diffstat (limited to 'ironic/tests/unit/drivers/modules')
56 files changed, 23428 insertions, 0 deletions
diff --git a/ironic/tests/unit/drivers/modules/__init__.py b/ironic/tests/unit/drivers/modules/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/__init__.py diff --git a/ironic/tests/unit/drivers/modules/amt/__init__.py b/ironic/tests/unit/drivers/modules/amt/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/amt/__init__.py diff --git a/ironic/tests/unit/drivers/modules/amt/test_common.py b/ironic/tests/unit/drivers/modules/amt/test_common.py new file mode 100644 index 000000000..d6ab30f5c --- /dev/null +++ b/ironic/tests/unit/drivers/modules/amt/test_common.py @@ -0,0 +1,173 @@ +# +# 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. + +""" +Test class for AMT Common +""" + +import mock +from oslo_config import cfg + +from ironic.common import exception +from ironic.drivers.modules.amt import common as amt_common +from ironic.drivers.modules.amt import resource_uris +from ironic.tests.unit import base +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.drac import utils as test_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_amt_info() +CONF = cfg.CONF + + +class AMTCommonMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(AMTCommonMethodsTestCase, self).setUp() + self.node = obj_utils.create_test_node(self.context, + driver='fake_amt', + driver_info=INFO_DICT) + + def test_parse_driver_info(self): + info = amt_common.parse_driver_info(self.node) + + self.assertIsNotNone(info.get('address')) + self.assertIsNotNone(info.get('username')) + self.assertIsNotNone(info.get('password')) + self.assertIsNotNone(info.get('protocol')) + self.assertIsNotNone(info.get('uuid')) + + def test_parse_driver_info_missing_address(self): + del self.node.driver_info['amt_address'] + + self.assertRaises(exception.MissingParameterValue, + amt_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_username(self): + del self.node.driver_info['amt_username'] + + self.assertRaises(exception.MissingParameterValue, + amt_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_password(self): + del self.node.driver_info['amt_password'] + self.assertRaises(exception.MissingParameterValue, + amt_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_protocol(self): + del self.node.driver_info['amt_protocol'] + info = amt_common.parse_driver_info(self.node) + self.assertEqual('http', info.get('protocol')) + + def test_parse_driver_info_wrong_protocol(self): + self.node.driver_info['amt_protocol'] = 'fake-protocol' + self.assertRaises(exception.InvalidParameterValue, + amt_common.parse_driver_info, self.node) + + @mock.patch.object(amt_common, 'Client', spec_set=True, autospec=True) + def test_get_wsman_client(self, mock_client): + info = amt_common.parse_driver_info(self.node) + amt_common.get_wsman_client(self.node) + options = {'address': info['address'], + 'protocol': info['protocol'], + 'username': info['username'], + 'password': info['password']} + + mock_client.assert_called_once_with(**options) + + def test_xml_find(self): + namespace = 'http://fake' + value = 'fake_value' + test_xml = test_utils.build_soap_xml([{'test_element': value}], + namespace) + mock_doc = test_utils.mock_wsman_root(test_xml) + + result = amt_common.xml_find(mock_doc, namespace, 'test_element') + self.assertEqual(value, result.text) + + def test_xml_find_fail(self): + mock_doc = None + self.assertRaises(exception.AMTConnectFailure, + amt_common.xml_find, + mock_doc, 'namespace', 'test_element') + + +@mock.patch.object(amt_common, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC) +class AMTCommonClientTestCase(base.TestCase): + def setUp(self): + super(AMTCommonClientTestCase, self).setUp() + self.info = {key[4:]: INFO_DICT[key] for key in INFO_DICT.keys()} + + def test_wsman_get(self, mock_client_pywsman): + namespace = resource_uris.CIM_AssociatedPowerManagementService + result_xml = test_utils.build_soap_xml([{'PowerState': + '2'}], + namespace) + mock_doc = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.get.return_value = mock_doc + client = amt_common.Client(**self.info) + + client.wsman_get(namespace) + mock_pywsman.get.assert_called_once_with(mock.ANY, namespace) + + def test_wsman_get_fail(self, mock_client_pywsman): + namespace = amt_common._SOAP_ENVELOPE + result_xml = test_utils.build_soap_xml([{'Fault': 'fault'}], + namespace) + mock_doc = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.get.return_value = mock_doc + client = amt_common.Client(**self.info) + + self.assertRaises(exception.AMTFailure, client.wsman_get, namespace) + mock_pywsman.get.assert_called_once_with(mock.ANY, namespace) + + def test_wsman_invoke(self, mock_client_pywsman): + namespace = resource_uris.CIM_BootSourceSetting + result_xml = test_utils.build_soap_xml([{'ReturnValue': + '0'}], + namespace) + mock_doc = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_doc + method = 'ChangeBootOrder' + options = mock.Mock(spec_set=[]) + client = amt_common.Client(**self.info) + doc = None + client.wsman_invoke(options, namespace, method, doc) + mock_pywsman.invoke.assert_called_once_with(options, namespace, method) + doc = 'fake-input' + client.wsman_invoke(options, namespace, method, doc) + mock_pywsman.invoke.assert_called_with(options, namespace, method, doc) + + def test_wsman_invoke_fail(self, mock_client_pywsman): + namespace = resource_uris.CIM_BootSourceSetting + result_xml = test_utils.build_soap_xml([{'ReturnValue': + '2'}], + namespace) + mock_doc = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_doc + method = 'fake-method' + options = mock.Mock(spec_set=[]) + + client = amt_common.Client(**self.info) + + self.assertRaises(exception.AMTFailure, + client.wsman_invoke, + options, namespace, method) + mock_pywsman.invoke.assert_called_once_with(options, namespace, method) diff --git a/ironic/tests/unit/drivers/modules/amt/test_management.py b/ironic/tests/unit/drivers/modules/amt/test_management.py new file mode 100644 index 000000000..2935389a7 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/amt/test_management.py @@ -0,0 +1,233 @@ +# +# 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. + +""" +Test class for AMT ManagementInterface +""" + +import mock +from oslo_config import cfg + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.amt import common as amt_common +from ironic.drivers.modules.amt import management as amt_mgmt +from ironic.drivers.modules.amt import resource_uris +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.drac import utils as test_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_amt_info() +CONF = cfg.CONF + + +@mock.patch.object(amt_common, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC) +class AMTManagementInteralMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(AMTManagementInteralMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_amt') + self.node = obj_utils.create_test_node(self.context, + driver='fake_amt', + driver_info=INFO_DICT) + + def test__set_boot_device_order(self, mock_client_pywsman): + namespace = resource_uris.CIM_BootConfigSetting + device = boot_devices.PXE + result_xml = test_utils.build_soap_xml([{'ReturnValue': '0'}], + namespace) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + amt_mgmt._set_boot_device_order(self.node, device) + + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, namespace, 'ChangeBootOrder', mock.ANY) + + def test__set_boot_device_order_fail(self, mock_client_pywsman): + namespace = resource_uris.CIM_BootConfigSetting + device = boot_devices.PXE + result_xml = test_utils.build_soap_xml([{'ReturnValue': '2'}], + namespace) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + self.assertRaises(exception.AMTFailure, + amt_mgmt._set_boot_device_order, self.node, device) + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, namespace, 'ChangeBootOrder', mock.ANY) + + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = None + + self.assertRaises(exception.AMTConnectFailure, + amt_mgmt._set_boot_device_order, self.node, device) + + def test__enable_boot_config(self, mock_client_pywsman): + namespace = resource_uris.CIM_BootService + result_xml = test_utils.build_soap_xml([{'ReturnValue': '0'}], + namespace) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + amt_mgmt._enable_boot_config(self.node) + + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, namespace, 'SetBootConfigRole', mock.ANY) + + def test__enable_boot_config_fail(self, mock_client_pywsman): + namespace = resource_uris.CIM_BootService + result_xml = test_utils.build_soap_xml([{'ReturnValue': '2'}], + namespace) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + self.assertRaises(exception.AMTFailure, + amt_mgmt._enable_boot_config, self.node) + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, namespace, 'SetBootConfigRole', mock.ANY) + + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = None + + self.assertRaises(exception.AMTConnectFailure, + amt_mgmt._enable_boot_config, self.node) + + +class AMTManagementTestCase(db_base.DbTestCase): + + def setUp(self): + super(AMTManagementTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_amt') + self.info = INFO_DICT + self.node = obj_utils.create_test_node(self.context, + driver='fake_amt', + driver_info=self.info) + + def test_get_properties(self): + expected = amt_common.COMMON_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(amt_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.management.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(amt_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate_fail(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_drvinfo.side_effect = iter( + [exception.InvalidParameterValue('x')]) + self.assertRaises(exception.InvalidParameterValue, + task.driver.management.validate, + task) + + def test_get_supported_boot_devices(self): + expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.CDROM] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual( + sorted(expected), + sorted(task.driver.management. + get_supported_boot_devices(task))) + + def test_set_boot_device_one_time(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, 'pxe') + self.assertEqual('pxe', + task.node.driver_internal_info["amt_boot_device"]) + self.assertFalse( + task.node.driver_internal_info["amt_boot_persistent"]) + + def test_set_boot_device_persistent(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, 'pxe', + persistent=True) + self.assertEqual('pxe', + task.node.driver_internal_info["amt_boot_device"]) + self.assertTrue( + task.node.driver_internal_info["amt_boot_persistent"]) + + def test_set_boot_device_fail(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.management.set_boot_device, + task, 'fake-device') + + @mock.patch.object(amt_mgmt, '_enable_boot_config', spec_set=True, + autospec=True) + @mock.patch.object(amt_mgmt, '_set_boot_device_order', spec_set=True, + autospec=True) + def test_ensure_next_boot_device_one_time(self, mock_sbdo, mock_ebc): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + device = boot_devices.PXE + task.node.driver_internal_info['amt_boot_device'] = 'pxe' + task.driver.management.ensure_next_boot_device(task.node, device) + self.assertEqual('disk', + task.node.driver_internal_info["amt_boot_device"]) + self.assertTrue( + task.node.driver_internal_info["amt_boot_persistent"]) + mock_sbdo.assert_called_once_with(task.node, device) + mock_ebc.assert_called_once_with(task.node) + + @mock.patch.object(amt_mgmt, '_enable_boot_config', spec_set=True, + autospec=True) + @mock.patch.object(amt_mgmt, '_set_boot_device_order', spec_set=True, + autospec=True) + def test_ensure_next_boot_device_persistent(self, mock_sbdo, mock_ebc): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + device = boot_devices.PXE + task.node.driver_internal_info['amt_boot_device'] = 'pxe' + task.node.driver_internal_info['amt_boot_persistent'] = True + task.driver.management.ensure_next_boot_device(task.node, device) + self.assertEqual('pxe', + task.node.driver_internal_info["amt_boot_device"]) + self.assertTrue( + task.node.driver_internal_info["amt_boot_persistent"]) + mock_sbdo.assert_called_once_with(task.node, device) + mock_ebc.assert_called_once_with(task.node) + + def test_get_boot_device(self): + expected = {'boot_device': boot_devices.DISK, 'persistent': True} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, + task.driver.management.get_boot_device(task)) + + def test_get_sensor_data(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(NotImplementedError, + task.driver.management.get_sensors_data, + task) diff --git a/ironic/tests/unit/drivers/modules/amt/test_power.py b/ironic/tests/unit/drivers/modules/amt/test_power.py new file mode 100644 index 000000000..878f71d5f --- /dev/null +++ b/ironic/tests/unit/drivers/modules/amt/test_power.py @@ -0,0 +1,282 @@ +# +# 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. + +""" +Test class for AMT ManagementInterface +""" + +import mock +from oslo_config import cfg + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.amt import common as amt_common +from ironic.drivers.modules.amt import management as amt_mgmt +from ironic.drivers.modules.amt import power as amt_power +from ironic.drivers.modules.amt import resource_uris +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.drac import utils as test_utils +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_amt_info() +CONF = cfg.CONF + + +class AMTPowerInteralMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(AMTPowerInteralMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_amt') + self.info = INFO_DICT + self.node = obj_utils.create_test_node(self.context, + driver='fake_amt', + driver_info=self.info) + CONF.set_override('max_attempts', 2, 'amt') + CONF.set_override('action_wait', 0, 'amt') + + @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, + autospec=True) + def test__set_power_state(self, mock_client_pywsman): + namespace = resource_uris.CIM_PowerManagementService + mock_client = mock_client_pywsman.return_value + amt_power._set_power_state(self.node, states.POWER_ON) + mock_client.wsman_invoke.assert_called_once_with( + mock.ANY, namespace, 'RequestPowerStateChange', mock.ANY) + + @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, + autospec=True) + def test__set_power_state_fail(self, mock_client_pywsman): + mock_client = mock_client_pywsman.return_value + mock_client.wsman_invoke.side_effect = exception.AMTFailure('x') + self.assertRaises(exception.AMTFailure, + amt_power._set_power_state, + self.node, states.POWER_ON) + + @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, + autospec=True) + def test__power_status(self, mock_gwc): + namespace = resource_uris.CIM_AssociatedPowerManagementService + result_xml = test_utils.build_soap_xml([{'PowerState': + '2'}], + namespace) + mock_doc = test_utils.mock_wsman_root(result_xml) + mock_client = mock_gwc.return_value + mock_client.wsman_get.return_value = mock_doc + self.assertEqual( + states.POWER_ON, amt_power._power_status(self.node)) + + result_xml = test_utils.build_soap_xml([{'PowerState': + '8'}], + namespace) + mock_doc = test_utils.mock_wsman_root(result_xml) + mock_client = mock_gwc.return_value + mock_client.wsman_get.return_value = mock_doc + self.assertEqual( + states.POWER_OFF, amt_power._power_status(self.node)) + + result_xml = test_utils.build_soap_xml([{'PowerState': + '4'}], + namespace) + mock_doc = test_utils.mock_wsman_root(result_xml) + mock_client = mock_gwc.return_value + mock_client.wsman_get.return_value = mock_doc + self.assertEqual( + states.ERROR, amt_power._power_status(self.node)) + + @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, + autospec=True) + def test__power_status_fail(self, mock_gwc): + mock_client = mock_gwc.return_value + mock_client.wsman_get.side_effect = exception.AMTFailure('x') + self.assertRaises(exception.AMTFailure, + amt_power._power_status, + self.node) + + @mock.patch.object(amt_mgmt.AMTManagement, 'ensure_next_boot_device', + spec_set=True, autospec=True) + @mock.patch.object(amt_power, '_power_status', spec_set=True, + autospec=True) + @mock.patch.object(amt_power, '_set_power_state', spec_set=True, + autospec=True) + def test__set_and_wait_power_on_with_boot_device(self, mock_sps, + mock_ps, mock_enbd): + target_state = states.POWER_ON + boot_device = boot_devices.PXE + mock_ps.side_effect = iter([states.POWER_OFF, states.POWER_ON]) + mock_enbd.return_value = None + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_internal_info['amt_boot_device'] = boot_device + result = amt_power._set_and_wait(task, target_state) + self.assertEqual(states.POWER_ON, result) + mock_enbd.assert_called_with(task.driver.management, task.node, + boot_devices.PXE) + mock_sps.assert_called_once_with(task.node, states.POWER_ON) + mock_ps.assert_called_with(task.node) + + @mock.patch.object(amt_power, '_power_status', spec_set=True, + autospec=True) + @mock.patch.object(amt_power, '_set_power_state', spec_set=True, + autospec=True) + def test__set_and_wait_power_on_without_boot_device(self, mock_sps, + mock_ps): + target_state = states.POWER_ON + mock_ps.side_effect = iter([states.POWER_OFF, states.POWER_ON]) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(states.POWER_ON, + amt_power._set_and_wait(task, target_state)) + mock_sps.assert_called_once_with(task.node, states.POWER_ON) + mock_ps.assert_called_with(task.node) + + boot_device = boot_devices.DISK + self.node.driver_internal_info['amt_boot_device'] = boot_device + mock_ps.side_effect = iter([states.POWER_OFF, states.POWER_ON]) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(states.POWER_ON, + amt_power._set_and_wait(task, target_state)) + mock_sps.assert_called_with(task.node, states.POWER_ON) + mock_ps.assert_called_with(task.node) + + def test__set_and_wait_wrong_target_state(self): + target_state = 'fake-state' + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + amt_power._set_and_wait, task, target_state) + + @mock.patch.object(amt_power, '_power_status', spec_set=True, + autospec=True) + @mock.patch.object(amt_power, '_set_power_state', spec_set=True, + autospec=True) + def test__set_and_wait_exceed_iterations(self, mock_sps, + mock_ps): + target_state = states.POWER_ON + mock_ps.side_effect = iter([states.POWER_OFF, states.POWER_OFF, + states.POWER_OFF]) + mock_sps.return_value = exception.AMTFailure('x') + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.PowerStateFailure, + amt_power._set_and_wait, task, target_state) + mock_sps.assert_called_with(task.node, states.POWER_ON) + mock_ps.assert_called_with(task.node) + self.assertEqual(3, mock_ps.call_count) + + @mock.patch.object(amt_power, '_power_status', spec_set=True, + autospec=True) + def test__set_and_wait_already_target_state(self, mock_ps): + target_state = states.POWER_ON + mock_ps.side_effect = iter([states.POWER_ON]) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(states.POWER_ON, + amt_power._set_and_wait(task, target_state)) + mock_ps.assert_called_with(task.node) + + @mock.patch.object(amt_power, '_power_status', spec_set=True, + autospec=True) + @mock.patch.object(amt_power, '_set_power_state', spec_set=True, + autospec=True) + def test__set_and_wait_power_off(self, mock_sps, mock_ps): + target_state = states.POWER_OFF + mock_ps.side_effect = iter([states.POWER_ON, states.POWER_OFF]) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(states.POWER_OFF, + amt_power._set_and_wait(task, target_state)) + mock_sps.assert_called_once_with(task.node, states.POWER_OFF) + mock_ps.assert_called_with(task.node) + + +class AMTPowerTestCase(db_base.DbTestCase): + + def setUp(self): + super(AMTPowerTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_amt') + self.info = INFO_DICT + self.node = obj_utils.create_test_node(self.context, + driver='fake_amt', + driver_info=self.info) + + def test_get_properties(self): + expected = amt_common.COMMON_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(amt_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(amt_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate_fail(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_drvinfo.side_effect = iter( + [exception.InvalidParameterValue('x')]) + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, + task) + + @mock.patch.object(amt_power, '_power_status', spec_set=True, + autospec=True) + def test_get_power_state(self, mock_ps): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_ps.return_value = states.POWER_ON + self.assertEqual(states.POWER_ON, + task.driver.power.get_power_state(task)) + mock_ps.assert_called_once_with(task.node) + + @mock.patch.object(amt_power, '_set_and_wait', spec_set=True, + autospec=True) + def test_set_power_state(self, mock_saw): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + pstate = states.POWER_ON + mock_saw.return_value = states.POWER_ON + task.driver.power.set_power_state(task, pstate) + mock_saw.assert_called_once_with(task, pstate) + + @mock.patch.object(amt_power, '_set_and_wait', spec_set=True, + autospec=True) + def test_set_power_state_fail(self, mock_saw): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + pstate = states.POWER_ON + mock_saw.side_effect = iter([exception.PowerStateFailure('x')]) + self.assertRaises(exception.PowerStateFailure, + task.driver.power.set_power_state, + task, pstate) + mock_saw.assert_called_once_with(task, pstate) + + @mock.patch.object(amt_power, '_set_and_wait', spec_set=True, + autospec=True) + def test_reboot(self, mock_saw): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.reboot(task) + calls = [mock.call(task, states.POWER_OFF), + mock.call(task, states.POWER_ON)] + mock_saw.assert_has_calls(calls) diff --git a/ironic/tests/unit/drivers/modules/amt/test_vendor.py b/ironic/tests/unit/drivers/modules/amt/test_vendor.py new file mode 100644 index 000000000..07d893251 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/amt/test_vendor.py @@ -0,0 +1,132 @@ +# +# 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. + +"""Test class for AMT Vendor methods.""" + +import mock + +from ironic.common import boot_devices +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.amt import management as amt_mgmt +from ironic.drivers.modules import iscsi_deploy +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INFO_DICT = db_utils.get_test_amt_info() + + +class AMTPXEVendorPassthruTestCase(db_base.DbTestCase): + + def setUp(self): + super(AMTPXEVendorPassthruTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="pxe_amt") + self.node = obj_utils.create_test_node( + self.context, driver='pxe_amt', driver_info=INFO_DICT) + + def test_vendor_routes(self): + expected = ['heartbeat', 'pass_deploy_info', + 'pass_bootloader_install_info'] + 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(list(vendor_routes))) + + def test_driver_routes(self): + expected = ['lookup'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + driver_routes = task.driver.vendor.driver_routes + self.assertIsInstance(driver_routes, dict) + self.assertEqual(sorted(expected), sorted(list(driver_routes))) + + @mock.patch.object(amt_mgmt.AMTManagement, 'ensure_next_boot_device', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy.VendorPassthru, 'pass_deploy_info', + spec_set=True, autospec=True) + def test_vendorpassthru_pass_deploy_info_netboot(self, + mock_pxe_vendorpassthru, + mock_ensure): + kwargs = {'address': '123456'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYWAIT + task.node.target_provision_state = states.ACTIVE + task.node.instance_info['capabilities'] = { + "boot_option": "netboot" + } + task.driver.vendor.pass_deploy_info(task, **kwargs) + mock_ensure.assert_called_with( + task.driver.management, task.node, boot_devices.PXE) + mock_pxe_vendorpassthru.assert_called_once_with( + task.driver.vendor, task, **kwargs) + + @mock.patch.object(amt_mgmt.AMTManagement, 'ensure_next_boot_device', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy.VendorPassthru, 'pass_deploy_info', + spec_set=True, autospec=True) + def test_vendorpassthru_pass_deploy_info_localboot(self, + mock_pxe_vendorpassthru, + mock_ensure): + kwargs = {'address': '123456'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYWAIT + task.node.target_provision_state = states.ACTIVE + task.node.instance_info['capabilities'] = {"boot_option": "local"} + task.driver.vendor.pass_deploy_info(task, **kwargs) + self.assertFalse(mock_ensure.called) + mock_pxe_vendorpassthru.assert_called_once_with( + task.driver.vendor, task, **kwargs) + + @mock.patch.object(amt_mgmt.AMTManagement, 'ensure_next_boot_device', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy.VendorPassthru, 'continue_deploy', + spec_set=True, autospec=True) + def test_vendorpassthru_continue_deploy_netboot(self, + mock_pxe_vendorpassthru, + mock_ensure): + kwargs = {'address': '123456'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYWAIT + task.node.target_provision_state = states.ACTIVE + task.node.instance_info['capabilities'] = { + "boot_option": "netboot" + } + task.driver.vendor.continue_deploy(task, **kwargs) + mock_ensure.assert_called_with( + task.driver.management, task.node, boot_devices.PXE) + mock_pxe_vendorpassthru.assert_called_once_with( + task.driver.vendor, task, **kwargs) + + @mock.patch.object(amt_mgmt.AMTManagement, 'ensure_next_boot_device', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy.VendorPassthru, 'continue_deploy', + spec_set=True, autospec=True) + def test_vendorpassthru_continue_deploy_localboot(self, + mock_pxe_vendorpassthru, + mock_ensure): + kwargs = {'address': '123456'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYWAIT + task.node.target_provision_state = states.ACTIVE + task.node.instance_info['capabilities'] = {"boot_option": "local"} + task.driver.vendor.continue_deploy(task, **kwargs) + self.assertFalse(mock_ensure.called) + mock_pxe_vendorpassthru.assert_called_once_with( + task.driver.vendor, task, **kwargs) diff --git a/ironic/tests/unit/drivers/modules/cimc/__init__.py b/ironic/tests/unit/drivers/modules/cimc/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/cimc/__init__.py diff --git a/ironic/tests/unit/drivers/modules/cimc/test_common.py b/ironic/tests/unit/drivers/modules/cimc/test_common.py new file mode 100644 index 000000000..0924f81de --- /dev/null +++ b/ironic/tests/unit/drivers/modules/cimc/test_common.py @@ -0,0 +1,125 @@ +# Copyright 2015, Cisco Systems. +# +# 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 mock + +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.cimc import common as cimc_common +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +imcsdk = importutils.try_import('ImcSdk') + +CONF = cfg.CONF + + +class CIMCBaseTestCase(db_base.DbTestCase): + + def setUp(self): + super(CIMCBaseTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_cimc") + self.node = obj_utils.create_test_node( + self.context, + driver='fake_cimc', + driver_info=db_utils.get_test_cimc_info(), + instance_uuid="fake_uuid") + CONF.set_override('max_retry', 2, 'cimc') + CONF.set_override('action_interval', 0, 'cimc') + + +class ParseDriverInfoTestCase(CIMCBaseTestCase): + + def test_parse_driver_info(self): + info = cimc_common.parse_driver_info(self.node) + + self.assertIsNotNone(info.get('cimc_address')) + self.assertIsNotNone(info.get('cimc_username')) + self.assertIsNotNone(info.get('cimc_password')) + + def test_parse_driver_info_missing_address(self): + del self.node.driver_info['cimc_address'] + self.assertRaises(exception.MissingParameterValue, + cimc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_username(self): + del self.node.driver_info['cimc_username'] + self.assertRaises(exception.MissingParameterValue, + cimc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_password(self): + del self.node.driver_info['cimc_password'] + self.assertRaises(exception.MissingParameterValue, + cimc_common.parse_driver_info, self.node) + + +@mock.patch.object(cimc_common, 'cimc_handle', autospec=True) +class CIMCHandleLogin(CIMCBaseTestCase): + + def test_cimc_handle_login(self, mock_handle): + info = cimc_common.parse_driver_info(self.node) + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + cimc_common.handle_login(task, handle, info) + + handle.login.assert_called_once_with( + self.node.driver_info['cimc_address'], + self.node.driver_info['cimc_username'], + self.node.driver_info['cimc_password']) + + def test_cimc_handle_login_exception(self, mock_handle): + info = cimc_common.parse_driver_info(self.node) + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.login.side_effect = imcsdk.ImcException('Boom') + + self.assertRaises(exception.CIMCException, + cimc_common.handle_login, + task, handle, info) + + handle.login.assert_called_once_with( + self.node.driver_info['cimc_address'], + self.node.driver_info['cimc_username'], + self.node.driver_info['cimc_password']) + + +class CIMCHandleTestCase(CIMCBaseTestCase): + + @mock.patch.object(imcsdk, 'ImcHandle', autospec=True) + @mock.patch.object(cimc_common, 'handle_login', autospec=True) + def test_cimc_handle(self, mock_login, mock_handle): + mo_hand = mock.MagicMock() + mo_hand.username = self.node.driver_info.get('cimc_username') + mo_hand.password = self.node.driver_info.get('cimc_password') + mo_hand.name = self.node.driver_info.get('cimc_address') + mock_handle.return_value = mo_hand + info = cimc_common.parse_driver_info(self.node) + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with cimc_common.cimc_handle(task) as handle: + self.assertEqual(handle, mock_handle.return_value) + + mock_login.assert_called_once_with(task, mock_handle.return_value, + info) + mock_handle.return_value.logout.assert_called_once_with() diff --git a/ironic/tests/unit/drivers/modules/cimc/test_management.py b/ironic/tests/unit/drivers/modules/cimc/test_management.py new file mode 100644 index 000000000..3a8d53493 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/cimc/test_management.py @@ -0,0 +1,126 @@ +# Copyright 2015, Cisco Systems. +# +# 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 mock + +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.cimc import common +from ironic.tests.unit.drivers.modules.cimc import test_common + +imcsdk = importutils.try_import('ImcSdk') + + +@mock.patch.object(common, 'cimc_handle', autospec=True) +class CIMCManagementTestCase(test_common.CIMCBaseTestCase): + + def test_get_properties(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertEqual(common.COMMON_PROPERTIES, + task.driver.management.get_properties()) + + @mock.patch.object(common, "parse_driver_info", autospec=True) + def test_validate(self, mock_driver_info, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.validate(task) + mock_driver_info.assert_called_once_with(task.node) + + def test_get_supported_boot_devices(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected = [boot_devices.PXE, boot_devices.DISK, + boot_devices.CDROM] + result = task.driver.management.get_supported_boot_devices(task) + self.assertEqual(sorted(expected), sorted(result)) + + def test_get_boot_device(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.xml_query.return_value.error_code = None + mock_dev = mock.MagicMock() + mock_dev.Order = 1 + mock_dev.Rn = 'storage-read-write' + handle.xml_query().OutConfigs.child[0].child = [mock_dev] + + device = task.driver.management.get_boot_device(task) + self.assertEqual( + {'boot_device': boot_devices.DISK, 'persistent': True}, + device) + + def test_get_boot_device_fail(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.xml_query.return_value.error_code = None + mock_dev = mock.MagicMock() + mock_dev.Order = 1 + mock_dev.Rn = 'storage-read-write' + handle.xml_query().OutConfigs.child[0].child = [mock_dev] + + device = task.driver.management.get_boot_device(task) + + self.assertEqual( + {'boot_device': boot_devices.DISK, 'persistent': True}, + device) + + def test_set_boot_device(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.xml_query.return_value.error_code = None + task.driver.management.set_boot_device(task, boot_devices.DISK) + method = imcsdk.ImcCore.ExternalMethod("ConfigConfMo") + method.Cookie = handle.cookie + method.Dn = "sys/rack-unit-1/boot-policy" + method.InHierarchical = "true" + + config = imcsdk.Imc.ConfigConfig() + + bootMode = imcsdk.ImcCore.ManagedObject('lsbootStorage') + bootMode.set_attr("access", 'read-write') + bootMode.set_attr("type", 'storage') + bootMode.set_attr("Rn", 'storage-read-write') + bootMode.set_attr("order", "1") + + config.add_child(bootMode) + method.InConfig = config + + handle.xml_query.assert_called_once_with( + method, imcsdk.WriteXmlOption.DIRTY) + + def test_set_boot_device_fail(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + method = imcsdk.ImcCore.ExternalMethod("ConfigConfMo") + handle.xml_query.return_value.error_code = "404" + + self.assertRaises(exception.CIMCException, + task.driver.management.set_boot_device, + task, boot_devices.DISK) + + handle.xml_query.assert_called_once_with( + method, imcsdk.WriteXmlOption.DIRTY) + + def test_get_sensors_data(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(NotImplementedError, + task.driver.management.get_sensors_data, task) diff --git a/ironic/tests/unit/drivers/modules/cimc/test_power.py b/ironic/tests/unit/drivers/modules/cimc/test_power.py new file mode 100644 index 000000000..f6a05bb8d --- /dev/null +++ b/ironic/tests/unit/drivers/modules/cimc/test_power.py @@ -0,0 +1,302 @@ +# Copyright 2015, Cisco Systems. +# +# 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 mock + +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.cimc import common +from ironic.drivers.modules.cimc import power +from ironic.tests.unit.drivers.modules.cimc import test_common + +imcsdk = importutils.try_import('ImcSdk') + +CONF = cfg.CONF + + +@mock.patch.object(common, 'cimc_handle', autospec=True) +class WaitForStateChangeTestCase(test_common.CIMCBaseTestCase): + + def setUp(self): + super(WaitForStateChangeTestCase, self).setUp() + CONF.set_override('max_retry', 2, 'cimc') + CONF.set_override('action_interval', 0, 'cimc') + + def test__wait_for_state_change(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + mock_rack_unit = mock.MagicMock() + mock_rack_unit.get_attr.return_value = ( + imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON) + + handle.get_imc_managedobject.return_value = [mock_rack_unit] + + state = power._wait_for_state_change(states.POWER_ON, task) + + handle.get_imc_managedobject.assert_called_once_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + self.assertEqual(state, states.POWER_ON) + + def test__wait_for_state_change_fail(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + mock_rack_unit = mock.MagicMock() + mock_rack_unit.get_attr.return_value = ( + imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF) + + handle.get_imc_managedobject.return_value = [mock_rack_unit] + + state = power._wait_for_state_change(states.POWER_ON, task) + + calls = [ + mock.call(None, None, params={"Dn": "sys/rack-unit-1"}), + mock.call(None, None, params={"Dn": "sys/rack-unit-1"}) + ] + handle.get_imc_managedobject.assert_has_calls(calls) + self.assertEqual(state, states.ERROR) + + def test__wait_for_state_change_imc_exception(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.get_imc_managedobject.side_effect = ( + imcsdk.ImcException('Boom')) + + self.assertRaises( + exception.CIMCException, + power._wait_for_state_change, states.POWER_ON, task) + + handle.get_imc_managedobject.assert_called_once_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + +@mock.patch.object(common, 'cimc_handle', autospec=True) +class PowerTestCase(test_common.CIMCBaseTestCase): + + def test_get_properties(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertEqual(common.COMMON_PROPERTIES, + task.driver.power.get_properties()) + + @mock.patch.object(common, "parse_driver_info", autospec=True) + def test_validate(self, mock_driver_info, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.validate(task) + mock_driver_info.assert_called_once_with(task.node) + + def test_get_power_state(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + mock_rack_unit = mock.MagicMock() + mock_rack_unit.get_attr.return_value = ( + imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON) + + handle.get_imc_managedobject.return_value = [mock_rack_unit] + + state = task.driver.power.get_power_state(task) + + handle.get_imc_managedobject.assert_called_once_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + self.assertEqual(states.POWER_ON, state) + + def test_get_power_state_fail(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + mock_rack_unit = mock.MagicMock() + mock_rack_unit.get_attr.return_value = ( + imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON) + + handle.get_imc_managedobject.side_effect = ( + imcsdk.ImcException("boom")) + + self.assertRaises(exception.CIMCException, + task.driver.power.get_power_state, task) + + handle.get_imc_managedobject.assert_called_once_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + def test_set_power_state_invalid_state(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, states.ERROR) + + def test_set_power_state_reboot_ok(self, mock_handle): + hri = imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_HARD_RESET_IMMEDIATE + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + mock_rack_unit = mock.MagicMock() + mock_rack_unit.get_attr.side_effect = [ + imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF, + imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON + ] + handle.get_imc_managedobject.return_value = [mock_rack_unit] + + task.driver.power.set_power_state(task, states.REBOOT) + + handle.set_imc_managedobject.assert_called_once_with( + None, class_id="ComputeRackUnit", + params={ + imcsdk.ComputeRackUnit.ADMIN_POWER: hri, + imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1" + }) + + handle.get_imc_managedobject.assert_called_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + def test_set_power_state_reboot_fail(self, mock_handle): + hri = imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_HARD_RESET_IMMEDIATE + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.get_imc_managedobject.side_effect = ( + imcsdk.ImcException("boom")) + + self.assertRaises(exception.CIMCException, + task.driver.power.set_power_state, + task, states.REBOOT) + + handle.set_imc_managedobject.assert_called_once_with( + None, class_id="ComputeRackUnit", + params={ + imcsdk.ComputeRackUnit.ADMIN_POWER: hri, + imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1" + }) + + handle.get_imc_managedobject.assert_called_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + def test_set_power_state_on_ok(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + mock_rack_unit = mock.MagicMock() + mock_rack_unit.get_attr.side_effect = [ + imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF, + imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON + ] + handle.get_imc_managedobject.return_value = [mock_rack_unit] + + task.driver.power.set_power_state(task, states.POWER_ON) + + handle.set_imc_managedobject.assert_called_once_with( + None, class_id="ComputeRackUnit", + params={ + imcsdk.ComputeRackUnit.ADMIN_POWER: + imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_UP, + imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1" + }) + + handle.get_imc_managedobject.assert_called_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + def test_set_power_state_on_fail(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.get_imc_managedobject.side_effect = ( + imcsdk.ImcException("boom")) + + self.assertRaises(exception.CIMCException, + task.driver.power.set_power_state, + task, states.POWER_ON) + + handle.set_imc_managedobject.assert_called_once_with( + None, class_id="ComputeRackUnit", + params={ + imcsdk.ComputeRackUnit.ADMIN_POWER: + imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_UP, + imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1" + }) + + handle.get_imc_managedobject.assert_called_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + def test_set_power_state_off_ok(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + mock_rack_unit = mock.MagicMock() + mock_rack_unit.get_attr.side_effect = [ + imcsdk.ComputeRackUnit.CONST_OPER_POWER_ON, + imcsdk.ComputeRackUnit.CONST_OPER_POWER_OFF + ] + handle.get_imc_managedobject.return_value = [mock_rack_unit] + + task.driver.power.set_power_state(task, states.POWER_OFF) + + handle.set_imc_managedobject.assert_called_once_with( + None, class_id="ComputeRackUnit", + params={ + imcsdk.ComputeRackUnit.ADMIN_POWER: + imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_DOWN, + imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1" + }) + + handle.get_imc_managedobject.assert_called_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + def test_set_power_state_off_fail(self, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + with mock_handle(task) as handle: + handle.get_imc_managedobject.side_effect = ( + imcsdk.ImcException("boom")) + + self.assertRaises(exception.CIMCException, + task.driver.power.set_power_state, + task, states.POWER_OFF) + + handle.set_imc_managedobject.assert_called_once_with( + None, class_id="ComputeRackUnit", + params={ + imcsdk.ComputeRackUnit.ADMIN_POWER: + imcsdk.ComputeRackUnit.CONST_ADMIN_POWER_DOWN, + imcsdk.ComputeRackUnit.DN: "sys/rack-unit-1" + }) + + handle.get_imc_managedobject.assert_called_with( + None, None, params={"Dn": "sys/rack-unit-1"}) + + @mock.patch.object(power.Power, "set_power_state", autospec=True) + @mock.patch.object(power.Power, "get_power_state", autospec=True) + def test_reboot_on(self, mock_get_state, mock_set_state, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_get_state.return_value = states.POWER_ON + task.driver.power.reboot(task) + mock_set_state.assert_called_with(mock.ANY, task, states.REBOOT) + + @mock.patch.object(power.Power, "set_power_state", autospec=True) + @mock.patch.object(power.Power, "get_power_state", autospec=True) + def test_reboot_off(self, mock_get_state, mock_set_state, mock_handle): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_get_state.return_value = states.POWER_OFF + task.driver.power.reboot(task) + mock_set_state.assert_called_with(mock.ANY, task, states.POWER_ON) diff --git a/ironic/tests/unit/drivers/modules/drac/__init__.py b/ironic/tests/unit/drivers/modules/drac/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/__init__.py diff --git a/ironic/tests/unit/drivers/modules/drac/bios_wsman_mock.py b/ironic/tests/unit/drivers/modules/drac/bios_wsman_mock.py new file mode 100644 index 000000000..245d27c01 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/bios_wsman_mock.py @@ -0,0 +1,273 @@ +# +# Copyright 2015 Dell, Inc. +# 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. + +""" +Test class for DRAC BIOS interface +""" + +from ironic.drivers.modules.drac import resource_uris + +Enumerations = { + resource_uris.DCIM_BIOSEnumeration: { + 'XML': """<ns0:Envelope +xmlns:ns0="http://www.w3.org/2003/05/soap-envelope" +xmlns:ns1="http://schemas.xmlsoap.org/ws/2004/08/addressing" +xmlns:ns2="http://schemas.xmlsoap.org/ws/2004/09/enumeration" +xmlns:ns3="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" +xmlns:ns4="http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_BIOSEnumeration" +xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <ns0:Header> + <ns1:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous +</ns1:To> + <ns1:Action> +http://schemas.xmlsoap.org/ws/2004/09/enumeration/EnumerateResponse</ns1:Action> + <ns1:RelatesTo>uuid:1f5cd907-0e6f-1e6f-8002-4f266e3acab8</ns1:RelatesTo> + <ns1:MessageID>uuid:219ca357-0e6f-1e6f-a828-f0e4fb722ab8</ns1:MessageID> + </ns0:Header> + <ns0:Body> + <ns2:EnumerateResponse> + <ns3:Items> + <ns4:DCIM_BIOSEnumeration> + <ns4:AttributeName>MemTest</ns4:AttributeName> + <ns4:CurrentValue>Disabled</ns4:CurrentValue> + <ns4:Dependency xsi:nil="true" /> + <ns4:DisplayOrder>310</ns4:DisplayOrder> + <ns4:FQDD>BIOS.Setup.1-1</ns4:FQDD> + <ns4:GroupDisplayName>Memory Settings</ns4:GroupDisplayName> + <ns4:GroupID>MemSettings</ns4:GroupID> + <ns4:InstanceID>BIOS.Setup.1-1:MemTest</ns4:InstanceID> + <ns4:IsReadOnly>false</ns4:IsReadOnly> + <ns4:PendingValue xsi:nil="true" /> + <ns4:PossibleValues>Enabled</ns4:PossibleValues> + <ns4:PossibleValues>Disabled</ns4:PossibleValues> + </ns4:DCIM_BIOSEnumeration> + <ns4:DCIM_BIOSEnumeration> + <ns4:AttributeDisplayName>C States</ns4:AttributeDisplayName> + <ns4:AttributeName>ProcCStates</ns4:AttributeName> + <ns4:CurrentValue>Disabled</ns4:CurrentValue> + <ns4:DisplayOrder>1706</ns4:DisplayOrder> + <ns4:FQDD>BIOS.Setup.1-1</ns4:FQDD> + <ns4:GroupDisplayName>System Profile Settings</ns4:GroupDisplayName> + <ns4:GroupID>SysProfileSettings</ns4:GroupID> + <ns4:InstanceID>BIOS.Setup.1-1:ProcCStates</ns4:InstanceID> + <ns4:IsReadOnly>true</ns4:IsReadOnly> + <ns4:PendingValue xsi:nil="true" /> + <ns4:PossibleValues>Enabled</ns4:PossibleValues> + <ns4:PossibleValues>Disabled</ns4:PossibleValues> + </ns4:DCIM_BIOSEnumeration> + </ns3:Items> + </ns2:EnumerateResponse> + </ns0:Body> + </ns0:Envelope>""", + 'Dict': { + 'MemTest': { + 'name': 'MemTest', + 'current_value': 'Disabled', + 'pending_value': None, + 'read_only': False, + 'possible_values': ['Disabled', 'Enabled']}, + 'ProcCStates': { + 'name': 'ProcCStates', + 'current_value': 'Disabled', + 'pending_value': None, + 'read_only': True, + 'possible_values': ['Disabled', 'Enabled']}}}, + resource_uris.DCIM_BIOSString: { + 'XML': """<ns0:Envelope +xmlns:ns0="http://www.w3.org/2003/05/soap-envelope" +xmlns:ns1="http://schemas.xmlsoap.org/ws/2004/08/addressing" +xmlns:ns2="http://schemas.xmlsoap.org/ws/2004/09/enumeration" +xmlns:ns3="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" +xmlns:ns4="http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_BIOSString" +xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <ns0:Header> + <ns1:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous +</ns1:To> + <ns1:Action> +http://schemas.xmlsoap.org/ws/2004/09/enumeration/EnumerateResponse +</ns1:Action> + <ns1:RelatesTo>uuid:1f877bcb-0e6f-1e6f-8004-4f266e3acab8</ns1:RelatesTo> + <ns1:MessageID>uuid:21bea321-0e6f-1e6f-a82b-f0e4fb722ab8</ns1:MessageID> + </ns0:Header> + <ns0:Body> + <ns2:EnumerateResponse> + <ns3:Items> + <ns4:DCIM_BIOSString> + <ns4:AttributeName>SystemModelName</ns4:AttributeName> + <ns4:CurrentValue>PowerEdge R630</ns4:CurrentValue> + <ns4:Dependency xsi:nil="true" /> + <ns4:DisplayOrder>201</ns4:DisplayOrder> + <ns4:FQDD>BIOS.Setup.1-1</ns4:FQDD> + <ns4:GroupDisplayName>System Information</ns4:GroupDisplayName> + <ns4:GroupID>SysInformation</ns4:GroupID> + <ns4:InstanceID>BIOS.Setup.1-1:SystemModelName</ns4:InstanceID> + <ns4:IsReadOnly>true</ns4:IsReadOnly> + <ns4:MaxLength>40</ns4:MaxLength> + <ns4:MinLength>0</ns4:MinLength> + <ns4:PendingValue xsi:nil="true" /> + <ns4:ValueExpression xsi:nil="true" /> + </ns4:DCIM_BIOSString> + <ns4:DCIM_BIOSString> + <ns4:AttributeName>SystemModelName2</ns4:AttributeName> + <ns4:CurrentValue>PowerEdge R630</ns4:CurrentValue> + <ns4:Dependency xsi:nil="true" /> + <ns4:DisplayOrder>201</ns4:DisplayOrder> + <ns4:FQDD>BIOS.Setup.1-1</ns4:FQDD> + <ns4:GroupDisplayName>System Information</ns4:GroupDisplayName> + <ns4:GroupID>SysInformation</ns4:GroupID> + <ns4:InstanceID>BIOS.Setup.1-1:SystemModelName2</ns4:InstanceID> + <ns4:IsReadOnly>true</ns4:IsReadOnly> + <ns4:MaxLength>40</ns4:MaxLength> + <ns4:MinLength>0</ns4:MinLength> + <ns4:PendingValue xsi:nil="true" /> + </ns4:DCIM_BIOSString> + <ns4:DCIM_BIOSString> + <ns4:AttributeDisplayName>Asset Tag</ns4:AttributeDisplayName> + <ns4:AttributeName>AssetTag</ns4:AttributeName> + <ns4:CurrentValue xsi:nil="true" /> + <ns4:Dependency xsi:nil="true" /> + <ns4:DisplayOrder>1903</ns4:DisplayOrder> + <ns4:FQDD>BIOS.Setup.1-1</ns4:FQDD> + <ns4:GroupDisplayName>Miscellaneous Settings</ns4:GroupDisplayName> + <ns4:GroupID>MiscSettings</ns4:GroupID> + <ns4:InstanceID>BIOS.Setup.1-1:AssetTag</ns4:InstanceID> + <ns4:IsReadOnly>false</ns4:IsReadOnly> + <ns4:MaxLength>63</ns4:MaxLength> + <ns4:MinLength>0</ns4:MinLength> + <ns4:PendingValue xsi:nil="true" /> + <ns4:ValueExpression>^[ -~]{0,63}$</ns4:ValueExpression> + </ns4:DCIM_BIOSString> + </ns3:Items> + <ns2:EnumerationContext /> + <ns3:EndOfSequence /> + </ns2:EnumerateResponse> + </ns0:Body> + </ns0:Envelope>""", + 'Dict': { + 'SystemModelName': { + 'name': 'SystemModelName', + 'current_value': 'PowerEdge R630', + 'pending_value': None, + 'read_only': True, + 'min_length': 0, + 'max_length': 40, + 'pcre_regex': None}, + 'SystemModelName2': { + 'name': 'SystemModelName2', + 'current_value': 'PowerEdge R630', + 'pending_value': None, + 'read_only': True, + 'min_length': 0, + 'max_length': 40, + 'pcre_regex': None}, + 'AssetTag': { + 'name': 'AssetTag', + 'current_value': None, + 'pending_value': None, + 'read_only': False, + 'min_length': 0, + 'max_length': 63, + 'pcre_regex': '^[ -~]{0,63}$'}}}, + resource_uris.DCIM_BIOSInteger: { + 'XML': """<ns0:Envelope +xmlns:ns0="http://www.w3.org/2003/05/soap-envelope" +xmlns:ns1="http://schemas.xmlsoap.org/ws/2004/08/addressing" +xmlns:ns2="http://schemas.xmlsoap.org/ws/2004/09/enumeration" +xmlns:ns3="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" +xmlns:ns4="http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_BIOSInteger" +xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <ns0:Header> + <ns1:To> +http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</ns1:To> + <ns1:Action> +http://schemas.xmlsoap.org/ws/2004/09/enumeration/EnumerateResponse</ns1:Action> + <ns1:RelatesTo>uuid:1fa60792-0e6f-1e6f-8005-4f266e3acab8</ns1:RelatesTo> + <ns1:MessageID>uuid:21ccf01d-0e6f-1e6f-a82d-f0e4fb722ab8</ns1:MessageID> + </ns0:Header> + <ns0:Body> + <ns2:EnumerateResponse> + <ns3:Items> + <ns4:DCIM_BIOSInteger> + <ns4:AttributeName>Proc1NumCores</ns4:AttributeName> + <ns4:CurrentValue>8</ns4:CurrentValue> + <ns4:Dependency xsi:nil="true" /> + <ns4:DisplayOrder>439</ns4:DisplayOrder> + <ns4:FQDD>BIOS.Setup.1-1</ns4:FQDD> + <ns4:GroupDisplayName>Processor Settings</ns4:GroupDisplayName> + <ns4:GroupID>ProcSettings</ns4:GroupID> + <ns4:InstanceID>BIOS.Setup.1-1:Proc1NumCores</ns4:InstanceID> + <ns4:IsReadOnly>true</ns4:IsReadOnly> + <ns4:LowerBound>0</ns4:LowerBound> + <ns4:PendingValue xsi:nil="true" /> + <ns4:UpperBound>65535</ns4:UpperBound> + </ns4:DCIM_BIOSInteger> + <ns4:DCIM_BIOSInteger> + <ns4:AttributeName>AcPwrRcvryUserDelay</ns4:AttributeName> + <ns4:CurrentValue>60</ns4:CurrentValue> + <ns4:DisplayOrder>1825</ns4:DisplayOrder> + <ns4:FQDD>BIOS.Setup.1-1</ns4:FQDD> + <ns4:GroupDisplayName>System Security</ns4:GroupDisplayName> + <ns4:GroupID>SysSecurity</ns4:GroupID> + <ns4:InstanceID>BIOS.Setup.1-1:AcPwrRcvryUserDelay</ns4:InstanceID> + <ns4:IsReadOnly>false</ns4:IsReadOnly> + <ns4:LowerBound>60</ns4:LowerBound> + <ns4:PendingValue xsi:nil="true" /> + <ns4:UpperBound>240</ns4:UpperBound> + </ns4:DCIM_BIOSInteger> + </ns3:Items> + <ns2:EnumerationContext /> + <ns3:EndOfSequence /> + </ns2:EnumerateResponse> + </ns0:Body> + </ns0:Envelope>""", + 'Dict': { + 'Proc1NumCores': { + 'name': 'Proc1NumCores', + 'current_value': 8, + 'pending_value': None, + 'read_only': True, + 'lower_bound': 0, + 'upper_bound': 65535}, + 'AcPwrRcvryUserDelay': { + 'name': 'AcPwrRcvryUserDelay', + 'current_value': 60, + 'pending_value': None, + 'read_only': False, + 'lower_bound': 60, + 'upper_bound': 240}}}} + +Invoke_Commit = """<ns0:Envelope +xmlns:ns0="http://www.w3.org/2003/05/soap-envelope" +xmlns:ns1="http://schemas.xmlsoap.org/ws/2004/08/addressing" +xmlns:ns2="http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_BIOSService"> + <ns0:Header> + <ns1:To> +http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</ns1:To> + <ns1:Action> +http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_BIOSService/SetAttributesResponse</ns1:Action> + <ns1:RelatesTo>uuid:42baa476-0ee9-1ee9-8020-4f266e3acab8</ns1:RelatesTo> + <ns1:MessageID>uuid:fadae2f8-0eea-1eea-9626-76a8f1d9bed4</ns1:MessageID> + </ns0:Header> + <ns0:Body> + <ns2:SetAttributes_OUTPUT> + <ns2:Message>The command was successful.</ns2:Message> + <ns2:MessageID>BIOS001</ns2:MessageID> + <ns2:RebootRequired>Yes</ns2:RebootRequired> + <ns2:ReturnValue>0</ns2:ReturnValue> + <ns2:SetResult>Set PendingValue</ns2:SetResult> + </ns2:SetAttributes_OUTPUT> + </ns0:Body> +</ns0:Envelope>""" diff --git a/ironic/tests/unit/drivers/modules/drac/test_bios.py b/ironic/tests/unit/drivers/modules/drac/test_bios.py new file mode 100644 index 000000000..8d62c152e --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/test_bios.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2015 Dell, Inc. +# 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. + +""" +Test class for DRAC BIOS interface +""" + +import mock + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.drac import bios +from ironic.drivers.modules.drac import client as drac_client +from ironic.drivers.modules.drac import management as drac_mgmt +from ironic.drivers.modules.drac import resource_uris +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.drac import bios_wsman_mock +from ironic.tests.unit.drivers.modules.drac import utils as test_utils +from ironic.tests.unit.objects import utils as obj_utils +from six.moves.urllib.parse import unquote + +FAKE_DRAC = db_utils.get_test_drac_info() + + +def _base_config(responses=[]): + for resource in [resource_uris.DCIM_BIOSEnumeration, + resource_uris.DCIM_BIOSString, + resource_uris.DCIM_BIOSInteger]: + xml_root = test_utils.mock_wsman_root( + bios_wsman_mock.Enumerations[resource]['XML']) + responses.append(xml_root) + return responses + + +def _set_config(responses=[]): + ccj_xml = test_utils.build_soap_xml([{'DCIM_LifecycleJob': + {'Name': 'fake'}}], + resource_uris.DCIM_LifecycleJob) + responses.append(test_utils.mock_wsman_root(ccj_xml)) + return _base_config(responses) + + +def _mock_pywsman_responses(client, responses): + mpw = client.Client.return_value + mpw.enumerate.side_effect = responses + return mpw + + +@mock.patch.object(drac_client, 'pywsman') +class DracBiosTestCase(db_base.DbTestCase): + + def setUp(self): + super(DracBiosTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_drac') + self.node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=FAKE_DRAC) + + def test_get_config(self, client): + _mock_pywsman_responses(client, _base_config()) + expected = {} + for resource in [resource_uris.DCIM_BIOSEnumeration, + resource_uris.DCIM_BIOSString, + resource_uris.DCIM_BIOSInteger]: + expected.update(bios_wsman_mock.Enumerations[resource]['Dict']) + result = bios.get_config(self.node) + self.assertEqual(expected, result) + + def test_set_config_empty(self, client): + _mock_pywsman_responses(client, _set_config()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + res = bios.set_config(task) + self.assertFalse(res) + + def test_set_config_nochange(self, client): + _mock_pywsman_responses(client, _set_config()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + res = bios.set_config(task, + MemTest='Disabled', + ProcCStates='Disabled', + SystemModelName='PowerEdge R630', + AssetTag=None, + Proc1NumCores=8, + AcPwrRcvryUserDelay=60) + self.assertFalse(res) + + def test_set_config_ro(self, client): + _mock_pywsman_responses(client, _set_config()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracOperationFailed, + bios.set_config, task, + ProcCStates="Enabled") + + def test_set_config_enum_invalid(self, client): + _mock_pywsman_responses(client, _set_config()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracOperationFailed, + bios.set_config, task, + MemTest="Never") + + def test_set_config_string_toolong(self, client): + _mock_pywsman_responses(client, _set_config()) + tag = ('Never have I seen such a silly long asset tag! ' + 'It is really rather ridiculous, don\'t you think?') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracOperationFailed, + bios.set_config, task, + AssetTag=tag) + + def test_set_config_string_nomatch(self, client): + _mock_pywsman_responses(client, _set_config()) + tag = unquote('%80') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracOperationFailed, + bios.set_config, task, + AssetTag=tag) + + def test_set_config_integer_toosmall(self, client): + _mock_pywsman_responses(client, _set_config()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracOperationFailed, + bios.set_config, task, + AcPwrRcvryUserDelay=0) + + def test_set_config_integer_toobig(self, client): + _mock_pywsman_responses(client, _set_config()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracOperationFailed, + bios.set_config, task, + AcPwrRcvryUserDelay=600) + + def test_set_config_needreboot(self, client): + mock_pywsman = _mock_pywsman_responses(client, _set_config()) + invoke_xml = test_utils.mock_wsman_root( + bios_wsman_mock.Invoke_Commit) + # TODO(victor-lowther) This needs more work. + # Specifically, we will need to verify that + # invoke was handed the XML blob we expected. + mock_pywsman.invoke.return_value = invoke_xml + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + res = bios.set_config(task, + AssetTag="An Asset Tag", + MemTest="Enabled") + self.assertTrue(res) + + @mock.patch.object(drac_mgmt, 'check_for_config_job', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, 'create_config_job', spec_set=True, + autospec=True) + def test_commit_config(self, mock_ccj, mock_cfcj, client): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + bios.commit_config(task) + self.assertTrue(mock_cfcj.called) + self.assertTrue(mock_ccj.called) + + @mock.patch.object(drac_client.Client, 'wsman_invoke', spec_set=True, + autospec=True) + def test_abandon_config(self, mock_wi, client): + _mock_pywsman_responses(client, _set_config()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + bios.abandon_config(task) + self.assertTrue(mock_wi.called) diff --git a/ironic/tests/unit/drivers/modules/drac/test_client.py b/ironic/tests/unit/drivers/modules/drac/test_client.py new file mode 100644 index 000000000..c31a1b78e --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/test_client.py @@ -0,0 +1,256 @@ +# +# 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. + +""" +Test class for DRAC client wrapper. +""" + +import time +from xml.etree import ElementTree + +import mock + +from ironic.common import exception +from ironic.drivers.modules.drac import client as drac_client +from ironic.tests.unit import base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.drac import utils as test_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs + +INFO_DICT = db_utils.get_test_drac_info() + + +@mock.patch.object(drac_client, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC) +class DracClientTestCase(base.TestCase): + + def setUp(self): + super(DracClientTestCase, self).setUp() + self.resource_uri = 'http://foo/wsman' + + def test_wsman_enumerate(self, mock_client_pywsman): + mock_xml = test_utils.mock_wsman_root('<test></test>') + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.enumerate.return_value = mock_xml + + client = drac_client.Client(**INFO_DICT) + client.wsman_enumerate(self.resource_uri) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_options.set_flags.assert_called_once_with( + mock_client_pywsman.FLAG_ENUMERATION_OPTIMIZATION) + mock_options.set_max_elements.assert_called_once_with(100) + mock_pywsman_client.enumerate.assert_called_once_with( + mock_options, None, self.resource_uri) + mock_xml.context.assert_called_once_with() + + @mock.patch.object(time, 'sleep', lambda seconds: None) + def test_wsman_enumerate_retry(self, mock_client_pywsman): + mock_xml = test_utils.mock_wsman_root('<test></test>') + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.enumerate.side_effect = [None, mock_xml] + + client = drac_client.Client(**INFO_DICT) + client.wsman_enumerate(self.resource_uri) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_options.set_flags.assert_called_once_with( + mock_client_pywsman.FLAG_ENUMERATION_OPTIMIZATION) + mock_options.set_max_elements.assert_called_once_with(100) + mock_pywsman_client.enumerate.assert_has_calls([ + mock.call(mock_options, None, self.resource_uri), + mock.call(mock_options, None, self.resource_uri) + ]) + mock_xml.context.assert_called_once_with() + + def test_wsman_enumerate_with_additional_pull(self, mock_client_pywsman): + mock_root = mock.Mock(spec=['string']) + mock_root.string.side_effect = [ + test_utils.build_soap_xml([{'item1': 'test1'}]), + test_utils.build_soap_xml([{'item2': 'test2'}]) + ] + mock_xml = mock.Mock(spec=['context', 'root']) + mock_xml.root.return_value = mock_root + mock_xml.context.side_effect = [42, 42, None] + + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.enumerate.return_value = mock_xml + mock_pywsman_client.pull.return_value = mock_xml + + client = drac_client.Client(**INFO_DICT) + result = client.wsman_enumerate(self.resource_uri) + + # assert the XML was merged + result_string = ElementTree.tostring(result) + self.assertIn(b'<item1>test1</item1>', result_string) + self.assertIn(b'<item2>test2</item2>', result_string) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_options.set_flags.assert_called_once_with( + mock_client_pywsman.FLAG_ENUMERATION_OPTIMIZATION) + mock_options.set_max_elements.assert_called_once_with(100) + mock_pywsman_client.enumerate.assert_called_once_with( + mock_options, None, self.resource_uri) + + def test_wsman_enumerate_filter_query(self, mock_client_pywsman): + mock_xml = test_utils.mock_wsman_root('<test></test>') + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.enumerate.return_value = mock_xml + + client = drac_client.Client(**INFO_DICT) + filter_query = 'SELECT * FROM foo' + client.wsman_enumerate(self.resource_uri, filter_query=filter_query) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_filter = mock_client_pywsman.Filter.return_value + mock_filter.simple.assert_called_once_with(mock.ANY, filter_query) + mock_pywsman_client.enumerate.assert_called_once_with( + mock_options, mock_filter, self.resource_uri) + mock_xml.context.assert_called_once_with() + + def test_wsman_enumerate_invalid_filter_dialect(self, mock_client_pywsman): + client = drac_client.Client(**INFO_DICT) + self.assertRaises(exception.DracInvalidFilterDialect, + client.wsman_enumerate, self.resource_uri, + filter_query='foo', + filter_dialect='invalid') + + def test_wsman_invoke(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], self.resource_uri) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + + method_name = 'method' + client = drac_client.Client(**INFO_DICT) + client.wsman_invoke(self.resource_uri, method_name) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_pywsman_client.invoke.assert_called_once_with( + mock_options, self.resource_uri, method_name, None) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + def test_wsman_invoke_retry(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], self.resource_uri) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.side_effect = [None, mock_xml] + + method_name = 'method' + client = drac_client.Client(**INFO_DICT) + client.wsman_invoke(self.resource_uri, method_name) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_pywsman_client.invoke.assert_has_calls([ + mock.call(mock_options, self.resource_uri, method_name, None), + mock.call(mock_options, self.resource_uri, method_name, None) + ]) + + def test_wsman_invoke_with_selectors(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], self.resource_uri) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + + method_name = 'method' + selectors = {'foo': 'bar'} + client = drac_client.Client(**INFO_DICT) + client.wsman_invoke(self.resource_uri, method_name, + selectors=selectors) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_pywsman_client.invoke.assert_called_once_with( + mock_options, self.resource_uri, method_name, None) + mock_options.add_selector.assert_called_once_with('foo', 'bar') + + def test_wsman_invoke_with_properties(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], self.resource_uri) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + + method_name = 'method' + properties = {'foo': 'bar'} + client = drac_client.Client(**INFO_DICT) + client.wsman_invoke(self.resource_uri, method_name, + properties=properties) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_pywsman_client.invoke.assert_called_once_with( + mock_options, self.resource_uri, method_name, None) + mock_options.add_property.assert_called_once_with('foo', 'bar') + + def test_wsman_invoke_with_properties_including_a_list( + self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], self.resource_uri) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + mock_request_xml = mock_client_pywsman.XmlDoc.return_value + + method_name = 'method' + properties = {'foo': ['bar', 'baz']} + client = drac_client.Client(**INFO_DICT) + client.wsman_invoke(self.resource_uri, method_name, + properties=properties) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_pywsman_client.invoke.assert_called_once_with( + mock_options, self.resource_uri, method_name, mock_request_xml) + mock_request_xml.root().add.assert_has_calls([ + mock.call(self.resource_uri, 'foo', 'bar'), + mock.call(self.resource_uri, 'foo', 'baz') + ]) + self.assertEqual(2, mock_request_xml.root().add.call_count) + + def test_wsman_invoke_receives_error_return_value( + self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_ERROR, + 'Message': 'error message'}], + self.resource_uri) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + + method_name = 'method' + client = drac_client.Client(**INFO_DICT) + self.assertRaises(exception.DracOperationFailed, + client.wsman_invoke, self.resource_uri, method_name) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_pywsman_client.invoke.assert_called_once_with( + mock_options, self.resource_uri, method_name, None) + + def test_wsman_invoke_receives_unexpected_return_value( + self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': '42'}], self.resource_uri) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + + method_name = 'method' + client = drac_client.Client(**INFO_DICT) + self.assertRaises(exception.DracUnexpectedReturnValue, + client.wsman_invoke, self.resource_uri, method_name, + {}, {}, drac_client.RET_SUCCESS) + + mock_options = mock_client_pywsman.ClientOptions.return_value + mock_pywsman_client.invoke.assert_called_once_with( + mock_options, self.resource_uri, method_name, None) diff --git a/ironic/tests/unit/drivers/modules/drac/test_common.py b/ironic/tests/unit/drivers/modules/drac/test_common.py new file mode 100644 index 000000000..2f2b7b7d4 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/test_common.py @@ -0,0 +1,135 @@ +# +# 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. + +""" +Test class for common methods used by DRAC modules. +""" + +from xml.etree import ElementTree + +from testtools.matchers import HasLength + +from ironic.common import exception +from ironic.drivers.modules.drac import common as drac_common +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 + +INFO_DICT = db_utils.get_test_drac_info() + + +class DracCommonMethodsTestCase(db_base.DbTestCase): + + def test_parse_driver_info(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + info = drac_common.parse_driver_info(node) + + self.assertIsNotNone(info.get('drac_host')) + self.assertIsNotNone(info.get('drac_port')) + self.assertIsNotNone(info.get('drac_path')) + self.assertIsNotNone(info.get('drac_protocol')) + self.assertIsNotNone(info.get('drac_username')) + self.assertIsNotNone(info.get('drac_password')) + + def test_parse_driver_info_missing_host(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + del node.driver_info['drac_host'] + self.assertRaises(exception.InvalidParameterValue, + drac_common.parse_driver_info, node) + + def test_parse_driver_info_missing_port(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + del node.driver_info['drac_port'] + + info = drac_common.parse_driver_info(node) + self.assertEqual(443, info.get('drac_port')) + + def test_parse_driver_info_invalid_port(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + node.driver_info['drac_port'] = 'foo' + self.assertRaises(exception.InvalidParameterValue, + drac_common.parse_driver_info, node) + + def test_parse_driver_info_missing_path(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + del node.driver_info['drac_path'] + + info = drac_common.parse_driver_info(node) + self.assertEqual('/wsman', info.get('drac_path')) + + def test_parse_driver_info_missing_protocol(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + del node.driver_info['drac_protocol'] + + info = drac_common.parse_driver_info(node) + self.assertEqual('https', info.get('drac_protocol')) + + def test_parse_driver_info_missing_username(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + del node.driver_info['drac_username'] + self.assertRaises(exception.InvalidParameterValue, + drac_common.parse_driver_info, node) + + def test_parse_driver_info_missing_password(self): + node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + del node.driver_info['drac_password'] + self.assertRaises(exception.InvalidParameterValue, + drac_common.parse_driver_info, node) + + def test_find_xml(self): + namespace = 'http://fake' + value = 'fake_value' + test_doc = ElementTree.fromstring("""<Envelope xmlns:ns1="%(ns)s"> + <Body> + <ns1:test_element>%(value)s</ns1:test_element> + </Body> + </Envelope>""" % {'ns': namespace, 'value': value}) + + result = drac_common.find_xml(test_doc, 'test_element', namespace) + self.assertEqual(value, result.text) + + def test_find_xml_find_all(self): + namespace = 'http://fake' + value1 = 'fake_value1' + value2 = 'fake_value2' + test_doc = ElementTree.fromstring("""<Envelope xmlns:ns1="%(ns)s"> + <Body> + <ns1:test_element>%(value1)s</ns1:test_element> + <ns1:cat>meow</ns1:cat> + <ns1:test_element>%(value2)s</ns1:test_element> + <ns1:dog>bark</ns1:dog> + </Body> + </Envelope>""" % {'ns': namespace, 'value1': value1, + 'value2': value2}) + + result = drac_common.find_xml(test_doc, 'test_element', + namespace, find_all=True) + self.assertThat(result, HasLength(2)) + result_text = [v.text for v in result] + self.assertEqual(sorted([value1, value2]), sorted(result_text)) diff --git a/ironic/tests/unit/drivers/modules/drac/test_management.py b/ironic/tests/unit/drivers/modules/drac/test_management.py new file mode 100644 index 000000000..c8e348931 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/test_management.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Red Hat, Inc. +# 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. + +""" +Test class for DRAC ManagementInterface +""" + +import mock + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.drac import client as drac_client +from ironic.drivers.modules.drac import common as drac_common +from ironic.drivers.modules.drac import management as drac_mgmt +from ironic.drivers.modules.drac import resource_uris +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.drac import utils as test_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_drac_info() + + +@mock.patch.object(drac_client, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC) +class DracManagementInternalMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(DracManagementInternalMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_drac') + self.node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + + def test__get_next_boot_list(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'DCIM_BootConfigSetting': {'InstanceID': 'IPL', + 'IsNext': drac_mgmt.PERSISTENT}}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + expected = {'instance_id': 'IPL', 'is_next': drac_mgmt.PERSISTENT} + result = drac_mgmt._get_next_boot_list(self.node) + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_BootConfigSetting) + + def test__get_next_boot_list_onetime(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'DCIM_BootConfigSetting': {'InstanceID': 'IPL', + 'IsNext': drac_mgmt.PERSISTENT}}, + {'DCIM_BootConfigSetting': {'InstanceID': 'OneTime', + 'IsNext': drac_mgmt.ONE_TIME_BOOT}}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + expected = {'instance_id': 'OneTime', + 'is_next': drac_mgmt.ONE_TIME_BOOT} + result = drac_mgmt._get_next_boot_list(self.node) + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_BootConfigSetting) + + def test__check_for_config_job(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'DCIM_LifecycleJob': {'Name': 'fake'}}], + resource_uris.DCIM_LifecycleJob) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + result = drac_mgmt.check_for_config_job(self.node) + + self.assertIsNone(result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_LifecycleJob) + + def test__check_for_config_job_already_exist(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'DCIM_LifecycleJob': {'Name': 'BIOS.Setup.1-1', + 'JobStatus': 'scheduled', + 'InstanceID': 'fake'}}], + resource_uris.DCIM_LifecycleJob) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + self.assertRaises(exception.DracPendingConfigJobExists, + drac_mgmt.check_for_config_job, self.node) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_LifecycleJob) + + def test__check_for_config_job_not_exist(self, mock_client_pywsman): + job_statuses = ["Completed", "Completed with Errors", "Failed"] + for job_status in job_statuses: + result_xml = test_utils.build_soap_xml( + [{'DCIM_LifecycleJob': {'Name': 'BIOS.Setup.1-1', + 'JobStatus': job_status, + 'InstanceID': 'fake'}}], + resource_uris.DCIM_LifecycleJob) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + try: + drac_mgmt.check_for_config_job(self.node) + except (exception.DracClientError, + exception.DracPendingConfigJobExists): + self.fail("Failed to detect completed job due to " + "\"{}\" job status".format(job_status)) + + def test_create_config_job(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_CREATED}], + resource_uris.DCIM_BIOSService) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + result = drac_mgmt.create_config_job(self.node) + + self.assertIsNone(result) + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, resource_uris.DCIM_BIOSService, + 'CreateTargetedConfigJob', None) + + def test_create_config_job_error(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_ERROR, + 'Message': 'E_FAKE'}], + resource_uris.DCIM_BIOSService) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.invoke.return_value = mock_xml + + self.assertRaises(exception.DracOperationFailed, + drac_mgmt.create_config_job, self.node) + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, resource_uris.DCIM_BIOSService, + 'CreateTargetedConfigJob', None) + + def test__get_lifecycle_controller_version(self, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'DCIM_SystemView': {'LifecycleControllerVersion': '42'}}], + resource_uris.DCIM_SystemView) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + result = drac_mgmt._get_lifecycle_controller_version(self.node) + + self.assertEqual('42', result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_SystemView) + + +@mock.patch.object(drac_client, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC) +class DracManagementTestCase(db_base.DbTestCase): + + def setUp(self): + super(DracManagementTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_drac') + self.node = obj_utils.create_test_node(self.context, + driver='fake_drac', + driver_info=INFO_DICT) + self.driver = drac_mgmt.DracManagement() + self.task = mock.Mock(spec=['node']) + self.task.node = self.node + + def test_get_properties(self, mock_client_pywsman): + expected = drac_common.COMMON_PROPERTIES + self.assertEqual(expected, self.driver.get_properties()) + + def test_get_supported_boot_devices(self, mock_client_pywsman): + expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.CDROM] + self.assertEqual(sorted(expected), + sorted(self.driver. + get_supported_boot_devices(self.task))) + + @mock.patch.object(drac_mgmt, '_get_next_boot_list', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + def test_get_boot_device(self, mock_glcv, mock_gnbl, mock_client_pywsman): + controller_version = '2.1.5.0' + mock_glcv.return_value = controller_version + mock_gnbl.return_value = {'instance_id': 'OneTime', + 'is_next': drac_mgmt.ONE_TIME_BOOT} + + result_xml = test_utils.build_soap_xml( + [{'InstanceID': 'HardDisk'}], resource_uris.DCIM_BootSourceSetting) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + result = self.driver.get_boot_device(self.task) + expected = {'boot_device': boot_devices.DISK, 'persistent': False} + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_BootSourceSetting) + + @mock.patch.object(drac_mgmt, '_get_next_boot_list', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + def test_get_boot_device_persistent(self, mock_glcv, mock_gnbl, + mock_client_pywsman): + controller_version = '2.1.5.0' + mock_glcv.return_value = controller_version + mock_gnbl.return_value = {'instance_id': 'IPL', + 'is_next': drac_mgmt.PERSISTENT} + + result_xml = test_utils.build_soap_xml( + [{'InstanceID': 'NIC', 'BootSourceType': 'IPL'}], + resource_uris.DCIM_BootSourceSetting) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml + + result = self.driver.get_boot_device(self.task) + expected = {'boot_device': boot_devices.PXE, 'persistent': True} + + self.assertEqual(expected, result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_BootSourceSetting) + + @mock.patch.object(drac_client.Client, 'wsman_enumerate', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_next_boot_list', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + def test_get_boot_device_client_error(self, mock_glcv, mock_gnbl, mock_we, + mock_client_pywsman): + controller_version = '2.1.5.0' + mock_glcv.return_value = controller_version + mock_gnbl.return_value = {'instance_id': 'OneTime', + 'is_next': drac_mgmt.ONE_TIME_BOOT} + mock_we.side_effect = iter([exception.DracClientError('E_FAKE')]) + + self.assertRaises(exception.DracClientError, + self.driver.get_boot_device, self.task) + mock_we.assert_called_once_with( + mock.ANY, resource_uris.DCIM_BootSourceSetting, + filter_query=mock.ANY) + + @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, 'check_for_config_job', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, 'create_config_job', spec_set=True, + autospec=True) + def test_set_boot_device(self, mock_ccj, mock_cfcj, mock_glcv, mock_gbd, + mock_client_pywsman): + controller_version = '2.1.5.0' + mock_glcv.return_value = controller_version + mock_gbd.return_value = {'boot_device': boot_devices.PXE, + 'persistent': True} + result_xml_enum = test_utils.build_soap_xml( + [{'InstanceID': 'NIC', 'BootSourceType': 'IPL'}], + resource_uris.DCIM_BootSourceSetting) + result_xml_invk = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml_enum = test_utils.mock_wsman_root(result_xml_enum) + mock_xml_invk = test_utils.mock_wsman_root(result_xml_invk) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml_enum + mock_pywsman.invoke.return_value = mock_xml_invk + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + result = self.driver.set_boot_device(task, boot_devices.PXE) + + self.assertIsNone(result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_BootSourceSetting) + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, resource_uris.DCIM_BootConfigSetting, + 'ChangeBootOrderByInstanceID', None) + mock_glcv.assert_called_once_with(self.node) + mock_gbd.assert_called_once_with(self.node, controller_version) + mock_cfcj.assert_called_once_with(self.node) + mock_ccj.assert_called_once_with(self.node) + + @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, 'check_for_config_job', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, 'create_config_job', spec_set=True, + autospec=True) + def test_set_boot_device_fail(self, mock_ccj, mock_cfcj, mock_glcv, + mock_gbd, mock_client_pywsman): + controller_version = '2.1.5.0' + mock_glcv.return_value = controller_version + mock_gbd.return_value = {'boot_device': boot_devices.PXE, + 'persistent': True} + result_xml_enum = test_utils.build_soap_xml( + [{'InstanceID': 'NIC', 'BootSourceType': 'IPL'}], + resource_uris.DCIM_BootSourceSetting) + result_xml_invk = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_ERROR, 'Message': 'E_FAKE'}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml_enum = test_utils.mock_wsman_root(result_xml_enum) + mock_xml_invk = test_utils.mock_wsman_root(result_xml_invk) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml_enum + mock_pywsman.invoke.return_value = mock_xml_invk + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracOperationFailed, + self.driver.set_boot_device, task, + boot_devices.PXE) + + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_BootSourceSetting) + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, resource_uris.DCIM_BootConfigSetting, + 'ChangeBootOrderByInstanceID', None) + mock_glcv.assert_called_once_with(self.node) + mock_gbd.assert_called_once_with(self.node, controller_version) + mock_cfcj.assert_called_once_with(self.node) + self.assertFalse(mock_ccj.called) + + @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + @mock.patch.object(drac_client.Client, 'wsman_enumerate', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, 'check_for_config_job', spec_set=True, + autospec=True) + def test_set_boot_device_client_error(self, mock_cfcj, mock_we, mock_glcv, + mock_gbd, + mock_client_pywsman): + controller_version = '2.1.5.0' + mock_glcv.return_value = controller_version + mock_gbd.return_value = {'boot_device': boot_devices.PXE, + 'persistent': True} + mock_we.side_effect = iter([exception.DracClientError('E_FAKE')]) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + self.assertRaises(exception.DracClientError, + self.driver.set_boot_device, task, + boot_devices.PXE) + mock_glcv.assert_called_once_with(self.node) + mock_gbd.assert_called_once_with(self.node, controller_version) + mock_we.assert_called_once_with( + mock.ANY, resource_uris.DCIM_BootSourceSetting, + filter_query=mock.ANY) + + @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, 'check_for_config_job', spec_set=True, + autospec=True) + def test_set_boot_device_noop(self, mock_cfcj, mock_glcv, mock_gbd, + mock_client_pywsman): + controller_version = '2.1.5.0' + mock_glcv.return_value = controller_version + mock_gbd.return_value = {'boot_device': boot_devices.PXE, + 'persistent': False} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + result = self.driver.set_boot_device(task, boot_devices.PXE) + self.assertIsNone(result) + mock_glcv.assert_called_once_with(self.node) + mock_gbd.assert_called_once_with(self.node, controller_version) + self.assertFalse(mock_cfcj.called) + + def test_get_sensors_data(self, mock_client_pywsman): + self.assertRaises(NotImplementedError, + self.driver.get_sensors_data, self.task) + + @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, '_get_lifecycle_controller_version', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, 'check_for_config_job', spec_set=True, + autospec=True) + @mock.patch.object(drac_mgmt, 'create_config_job', spec_set=True, + autospec=True) + def test_set_boot_device_11g(self, mock_ccj, mock_cfcj, mock_glcv, + mock_gbd, mock_client_pywsman): + controller_version = '1.5.0.0' + mock_glcv.return_value = controller_version + mock_gbd.return_value = {'boot_device': boot_devices.PXE, + 'persistent': True} + result_xml_enum = test_utils.build_soap_xml( + [{'InstanceID': 'NIC'}], + resource_uris.DCIM_BootSourceSetting) + result_xml_invk = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], + resource_uris.DCIM_BootConfigSetting) + + mock_xml_enum = test_utils.mock_wsman_root(result_xml_enum) + mock_xml_invk = test_utils.mock_wsman_root(result_xml_invk) + mock_pywsman = mock_client_pywsman.Client.return_value + mock_pywsman.enumerate.return_value = mock_xml_enum + mock_pywsman.invoke.return_value = mock_xml_invk + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node = self.node + result = self.driver.set_boot_device(task, boot_devices.PXE) + + self.assertIsNone(result) + mock_pywsman.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_BootSourceSetting) + mock_pywsman.invoke.assert_called_once_with( + mock.ANY, resource_uris.DCIM_BootConfigSetting, + 'ChangeBootOrderByInstanceID', None) + mock_glcv.assert_called_once_with(self.node) + mock_gbd.assert_called_once_with(self.node, controller_version) + mock_cfcj.assert_called_once_with(self.node) + mock_ccj.assert_called_once_with(self.node) diff --git a/ironic/tests/unit/drivers/modules/drac/test_power.py b/ironic/tests/unit/drivers/modules/drac/test_power.py new file mode 100644 index 000000000..9b4587ef7 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/test_power.py @@ -0,0 +1,175 @@ +# +# 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. + +""" +Test class for DRAC Power Driver +""" + +import mock + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.drac import client as drac_client +from ironic.drivers.modules.drac import common as drac_common +from ironic.drivers.modules.drac import power as drac_power +from ironic.drivers.modules.drac import resource_uris +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.drac import utils as test_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs + +INFO_DICT = db_utils.get_test_drac_info() + + +@mock.patch.object(drac_client, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC) +@mock.patch.object(drac_power, 'pywsman', spec_set=mock_specs.PYWSMAN_SPEC) +class DracPowerInternalMethodsTestCase(base.DbTestCase): + + def setUp(self): + super(DracPowerInternalMethodsTestCase, self).setUp() + driver_info = INFO_DICT + self.node = db_utils.create_test_node( + driver='fake_drac', + driver_info=driver_info, + instance_uuid='instance_uuid_123') + + def test__get_power_state(self, mock_power_pywsman, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'EnabledState': '2'}], resource_uris.DCIM_ComputerSystem) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.enumerate.return_value = mock_xml + + self.assertEqual(states.POWER_ON, + drac_power._get_power_state(self.node)) + + mock_pywsman_client.enumerate.assert_called_once_with( + mock.ANY, mock.ANY, resource_uris.DCIM_ComputerSystem) + + def test__set_power_state(self, mock_power_pywsman, mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_SUCCESS}], + resource_uris.DCIM_ComputerSystem) + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + + mock_pywsman_clientopts = ( + mock_client_pywsman.ClientOptions.return_value) + + drac_power._set_power_state(self.node, states.POWER_ON) + + mock_pywsman_clientopts.add_selector.assert_has_calls([ + mock.call('CreationClassName', 'DCIM_ComputerSystem'), + mock.call('Name', 'srv:system') + ], any_order=True) + mock_pywsman_clientopts.add_property.assert_called_once_with( + 'RequestedState', '2') + + mock_pywsman_client.invoke.assert_called_once_with( + mock.ANY, resource_uris.DCIM_ComputerSystem, + 'RequestStateChange', None) + + def test__set_power_state_fail(self, mock_power_pywsman, + mock_client_pywsman): + result_xml = test_utils.build_soap_xml( + [{'ReturnValue': drac_client.RET_ERROR, + 'Message': 'error message'}], + resource_uris.DCIM_ComputerSystem) + + mock_xml = test_utils.mock_wsman_root(result_xml) + mock_pywsman_client = mock_client_pywsman.Client.return_value + mock_pywsman_client.invoke.return_value = mock_xml + + mock_pywsman_clientopts = ( + mock_client_pywsman.ClientOptions.return_value) + + self.assertRaises(exception.DracOperationFailed, + drac_power._set_power_state, self.node, + states.POWER_ON) + + mock_pywsman_clientopts.add_selector.assert_has_calls([ + mock.call('CreationClassName', 'DCIM_ComputerSystem'), + mock.call('Name', 'srv:system') + ], any_order=True) + mock_pywsman_clientopts.add_property.assert_called_once_with( + 'RequestedState', '2') + + mock_pywsman_client.invoke.assert_called_once_with( + mock.ANY, resource_uris.DCIM_ComputerSystem, + 'RequestStateChange', None) + + +class DracPowerTestCase(base.DbTestCase): + + def setUp(self): + super(DracPowerTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver="fake_drac") + self.node = db_utils.create_test_node( + driver='fake_drac', + driver_info=driver_info, + instance_uuid='instance_uuid_123') + + def test_get_properties(self): + expected = drac_common.COMMON_PROPERTIES + driver = drac_power.DracPower() + self.assertEqual(expected, driver.get_properties()) + + @mock.patch.object(drac_power, '_get_power_state', spec_set=True, + autospec=True) + def test_get_power_state(self, mock_get_power_state): + mock_get_power_state.return_value = states.POWER_ON + driver = drac_power.DracPower() + task = mock.Mock() + task.node.return_value = self.node + + self.assertEqual(states.POWER_ON, driver.get_power_state(task)) + mock_get_power_state.assert_called_once_with(task.node) + + @mock.patch.object(drac_power, '_set_power_state', spec_set=True, + autospec=True) + def test_set_power_state(self, mock_set_power_state): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + mock_set_power_state.assert_called_once_with(task.node, + states.POWER_ON) + + @mock.patch.object(drac_power, '_set_power_state', spec_set=True, + autospec=True) + @mock.patch.object(drac_power, '_get_power_state', spec_set=True, + autospec=True) + def test_reboot(self, mock_get_power_state, mock_set_power_state): + mock_get_power_state.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.reboot(task) + mock_set_power_state.assert_called_once_with(task.node, + states.REBOOT) + + @mock.patch.object(drac_power, '_set_power_state', spec_set=True, + autospec=True) + @mock.patch.object(drac_power, '_get_power_state', spec_set=True, + autospec=True) + def test_reboot_in_power_off(self, mock_get_power_state, + mock_set_power_state): + mock_get_power_state.return_value = states.POWER_OFF + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.reboot(task) + mock_set_power_state.assert_called_once_with(task.node, + states.POWER_ON) diff --git a/ironic/tests/unit/drivers/modules/drac/utils.py b/ironic/tests/unit/drivers/modules/drac/utils.py new file mode 100644 index 000000000..6338f7168 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/drac/utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Red Hat, Inc. +# 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. + +from xml.etree import ElementTree + +import mock + + +def build_soap_xml(items, namespace=None): + """Build a SOAP XML. + + :param items: a list of dictionaries where key is the element name + and the value is the element text. + :param namespace: the namespace for the elements, None for no + namespace. Defaults to None + :returns: a XML string. + + """ + + def _create_element(name, value=None): + xml_string = name + if namespace: + xml_string = "{%(namespace)s}%(item)s" % {'namespace': namespace, + 'item': xml_string} + + element = ElementTree.Element(xml_string) + element.text = value + return element + + soap_namespace = "http://www.w3.org/2003/05/soap-envelope" + envelope_element = ElementTree.Element("{%s}Envelope" % soap_namespace) + body_element = ElementTree.Element("{%s}Body" % soap_namespace) + + for item in items: + for i in item: + insertion_point = _create_element(i) + if isinstance(item[i], dict): + for j, value in item[i].items(): + insertion_point.append(_create_element(j, value)) + else: + insertion_point.text = item[i] + + body_element.append(insertion_point) + + envelope_element.append(body_element) + return ElementTree.tostring(envelope_element) + + +def mock_wsman_root(return_value): + """Helper function to mock the root() from wsman client.""" + mock_xml_root = mock.Mock(spec_set=['string']) + mock_xml_root.string.return_value = return_value + + mock_xml = mock.Mock(spec_set=['context', 'root']) + mock_xml.context.return_value = None + mock_xml.root.return_value = mock_xml_root + + return mock_xml diff --git a/ironic/tests/unit/drivers/modules/ilo/__init__.py b/ironic/tests/unit/drivers/modules/ilo/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/__init__.py diff --git a/ironic/tests/unit/drivers/modules/ilo/test_common.py b/ironic/tests/unit/drivers/modules/ilo/test_common.py new file mode 100644 index 000000000..10b1ad305 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/test_common.py @@ -0,0 +1,675 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# 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. + +"""Test class for common methods used by iLO modules.""" + +import os +import shutil +import tempfile + +import mock +from oslo_config import cfg +from oslo_utils import importutils +import six + +from ironic.common import exception +from ironic.common import images +from ironic.common import swift +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.drivers.modules.ilo import common as ilo_common +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +ilo_client = importutils.try_import('proliantutils.ilo.client') +ilo_error = importutils.try_import('proliantutils.exception') + +if six.PY3: + import io + file = io.BytesIO + + +CONF = cfg.CONF + + +class IloValidateParametersTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloValidateParametersTestCase, self).setUp() + self.node = obj_utils.create_test_node( + self.context, driver='fake_ilo', + driver_info=db_utils.get_test_ilo_info()) + + def test_parse_driver_info(self): + info = ilo_common.parse_driver_info(self.node) + + self.assertIsNotNone(info.get('ilo_address')) + self.assertIsNotNone(info.get('ilo_username')) + self.assertIsNotNone(info.get('ilo_password')) + self.assertIsNotNone(info.get('client_timeout')) + self.assertIsNotNone(info.get('client_port')) + + def test_parse_driver_info_missing_address(self): + del self.node.driver_info['ilo_address'] + self.assertRaises(exception.MissingParameterValue, + ilo_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_username(self): + del self.node.driver_info['ilo_username'] + self.assertRaises(exception.MissingParameterValue, + ilo_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_password(self): + del self.node.driver_info['ilo_password'] + self.assertRaises(exception.MissingParameterValue, + ilo_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_timeout(self): + self.node.driver_info['client_timeout'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + ilo_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_port(self): + self.node.driver_info['client_port'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + ilo_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_multiple_params(self): + del self.node.driver_info['ilo_password'] + del self.node.driver_info['ilo_address'] + try: + ilo_common.parse_driver_info(self.node) + self.fail("parse_driver_info did not throw exception.") + except exception.MissingParameterValue as e: + self.assertIn('ilo_password', str(e)) + self.assertIn('ilo_address', str(e)) + + def test_parse_driver_info_invalid_multiple_params(self): + self.node.driver_info['client_timeout'] = 'qwe' + self.node.driver_info['console_port'] = 'not-int' + try: + ilo_common.parse_driver_info(self.node) + self.fail("parse_driver_info did not throw exception.") + except exception.InvalidParameterValue as e: + self.assertIn('client_timeout', str(e)) + self.assertIn('console_port', str(e)) + + +class IloCommonMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloCommonMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ilo") + self.info = db_utils.get_test_ilo_info() + self.node = obj_utils.create_test_node( + self.context, driver='fake_ilo', driver_info=self.info) + + @mock.patch.object(ilo_client, 'IloClient', spec_set=True, + autospec=True) + def test_get_ilo_object(self, ilo_client_mock): + self.info['client_timeout'] = 60 + self.info['client_port'] = 443 + ilo_client_mock.return_value = 'ilo_object' + returned_ilo_object = ilo_common.get_ilo_object(self.node) + ilo_client_mock.assert_called_with( + self.info['ilo_address'], + self.info['ilo_username'], + self.info['ilo_password'], + self.info['client_timeout'], + self.info['client_port']) + self.assertEqual('ilo_object', returned_ilo_object) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_ilo_license(self, get_ilo_object_mock): + ilo_advanced_license = {'LICENSE_TYPE': 'iLO 3 Advanced'} + ilo_standard_license = {'LICENSE_TYPE': 'iLO 3'} + + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_all_licenses.return_value = ilo_advanced_license + + license = ilo_common.get_ilo_license(self.node) + self.assertEqual(ilo_common.ADVANCED_LICENSE, license) + + ilo_mock_object.get_all_licenses.return_value = ilo_standard_license + license = ilo_common.get_ilo_license(self.node) + self.assertEqual(ilo_common.STANDARD_LICENSE, license) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_ilo_license_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.get_all_licenses.side_effect = exc + self.assertRaises(exception.IloOperationError, + ilo_common.get_ilo_license, + self.node) + + def test_update_ipmi_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ipmi_info = { + "ipmi_address": "1.2.3.4", + "ipmi_username": "admin", + "ipmi_password": "fake", + "ipmi_terminal_port": 60 + } + self.info['console_port'] = 60 + task.node.driver_info = self.info + ilo_common.update_ipmi_properties(task) + actual_info = task.node.driver_info + expected_info = dict(self.info, **ipmi_info) + self.assertEqual(expected_info, actual_info) + + def test__get_floppy_image_name(self): + image_name_expected = 'image-' + self.node.uuid + image_name_actual = ilo_common._get_floppy_image_name(self.node) + self.assertEqual(image_name_expected, image_name_actual) + + @mock.patch.object(swift, 'SwiftAPI', spec_set=True, autospec=True) + @mock.patch.object(images, 'create_vfat_image', spec_set=True, + autospec=True) + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + def test__prepare_floppy_image(self, tempfile_mock, fatimage_mock, + swift_api_mock): + mock_image_file_handle = mock.MagicMock(spec=file) + mock_image_file_obj = mock.MagicMock(spec=file) + mock_image_file_obj.name = 'image-tmp-file' + mock_image_file_handle.__enter__.return_value = mock_image_file_obj + + tempfile_mock.return_value = mock_image_file_handle + + swift_obj_mock = swift_api_mock.return_value + self.config(swift_ilo_container='ilo_cont', group='ilo') + self.config(swift_object_expiry_timeout=1, group='ilo') + deploy_args = {'arg1': 'val1', 'arg2': 'val2'} + swift_obj_mock.get_temp_url.return_value = 'temp-url' + timeout = CONF.ilo.swift_object_expiry_timeout + object_headers = {'X-Delete-After': timeout} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + temp_url = ilo_common._prepare_floppy_image(task, deploy_args) + node_uuid = task.node.uuid + + object_name = 'image-' + node_uuid + fatimage_mock.assert_called_once_with('image-tmp-file', + parameters=deploy_args) + + swift_obj_mock.create_object.assert_called_once_with( + 'ilo_cont', object_name, 'image-tmp-file', + object_headers=object_headers) + swift_obj_mock.get_temp_url.assert_called_once_with( + 'ilo_cont', object_name, timeout) + self.assertEqual('temp-url', temp_url) + + @mock.patch.object(ilo_common, 'copy_image_to_web_server', + spec_set=True, autospec=True) + @mock.patch.object(images, 'create_vfat_image', spec_set=True, + autospec=True) + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + def test__prepare_floppy_image_use_webserver(self, tempfile_mock, + fatimage_mock, + copy_mock): + mock_image_file_handle = mock.MagicMock(spec=file) + mock_image_file_obj = mock.MagicMock(spec=file) + mock_image_file_obj.name = 'image-tmp-file' + mock_image_file_handle.__enter__.return_value = mock_image_file_obj + + tempfile_mock.return_value = mock_image_file_handle + self.config(use_web_server_for_images=True, group='ilo') + deploy_args = {'arg1': 'val1', 'arg2': 'val2'} + CONF.deploy.http_url = "http://abc.com/httpboot" + CONF.deploy.http_root = "/httpboot" + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + node_uuid = task.node.uuid + object_name = 'image-' + node_uuid + http_url = CONF.deploy.http_url + '/' + object_name + copy_mock.return_value = "http://abc.com/httpboot/" + object_name + temp_url = ilo_common._prepare_floppy_image(task, deploy_args) + + fatimage_mock.assert_called_once_with('image-tmp-file', + parameters=deploy_args) + copy_mock.assert_called_once_with('image-tmp-file', object_name) + self.assertEqual(http_url, temp_url) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_attach_vmedia(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + insert_media_mock = ilo_mock_object.insert_virtual_media + set_status_mock = ilo_mock_object.set_vm_status + + ilo_common.attach_vmedia(self.node, 'FLOPPY', 'url') + insert_media_mock.assert_called_once_with('url', device='FLOPPY') + set_status_mock.assert_called_once_with( + device='FLOPPY', boot_option='CONNECT', write_protect='YES') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_attach_vmedia_fails(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + set_status_mock = ilo_mock_object.set_vm_status + exc = ilo_error.IloError('error') + set_status_mock.side_effect = exc + self.assertRaises(exception.IloOperationError, + ilo_common.attach_vmedia, self.node, + 'FLOPPY', 'url') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_boot_mode(self, get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + get_pending_boot_mode_mock = ilo_object_mock.get_pending_boot_mode + set_pending_boot_mode_mock = ilo_object_mock.set_pending_boot_mode + get_pending_boot_mode_mock.return_value = 'LEGACY' + ilo_common.set_boot_mode(self.node, 'uefi') + get_ilo_object_mock.assert_called_once_with(self.node) + get_pending_boot_mode_mock.assert_called_once_with() + set_pending_boot_mode_mock.assert_called_once_with('UEFI') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_boot_mode_without_set_pending_boot_mode(self, + get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + get_pending_boot_mode_mock = ilo_object_mock.get_pending_boot_mode + get_pending_boot_mode_mock.return_value = 'LEGACY' + ilo_common.set_boot_mode(self.node, 'bios') + get_ilo_object_mock.assert_called_once_with(self.node) + get_pending_boot_mode_mock.assert_called_once_with() + self.assertFalse(ilo_object_mock.set_pending_boot_mode.called) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_boot_mode_with_IloOperationError(self, + get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + get_pending_boot_mode_mock = ilo_object_mock.get_pending_boot_mode + get_pending_boot_mode_mock.return_value = 'UEFI' + set_pending_boot_mode_mock = ilo_object_mock.set_pending_boot_mode + exc = ilo_error.IloError('error') + set_pending_boot_mode_mock.side_effect = exc + self.assertRaises(exception.IloOperationError, + ilo_common.set_boot_mode, self.node, 'bios') + get_ilo_object_mock.assert_called_once_with(self.node) + get_pending_boot_mode_mock.assert_called_once_with() + + @mock.patch.object(ilo_common, 'set_boot_mode', spec_set=True, + autospec=True) + def test_update_boot_mode_instance_info_exists(self, + set_boot_mode_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['deploy_boot_mode'] = 'bios' + ilo_common.update_boot_mode(task) + set_boot_mode_mock.assert_called_once_with(task.node, 'bios') + + @mock.patch.object(ilo_common, 'set_boot_mode', spec_set=True, + autospec=True) + def test_update_boot_mode_capabilities_exist(self, + set_boot_mode_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties['capabilities'] = 'boot_mode:bios' + ilo_common.update_boot_mode(task) + set_boot_mode_mock.assert_called_once_with(task.node, 'bios') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_update_boot_mode(self, get_ilo_object_mock): + ilo_mock_obj = get_ilo_object_mock.return_value + ilo_mock_obj.get_pending_boot_mode.return_value = 'LEGACY' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.update_boot_mode(task) + get_ilo_object_mock.assert_called_once_with(task.node) + ilo_mock_obj.get_pending_boot_mode.assert_called_once_with() + self.assertEqual('bios', + task.node.instance_info['deploy_boot_mode']) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_update_boot_mode_unknown(self, + get_ilo_object_mock): + ilo_mock_obj = get_ilo_object_mock.return_value + ilo_mock_obj.get_pending_boot_mode.return_value = 'UNKNOWN' + set_pending_boot_mode_mock = ilo_mock_obj.set_pending_boot_mode + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.update_boot_mode(task) + get_ilo_object_mock.assert_called_once_with(task.node) + ilo_mock_obj.get_pending_boot_mode.assert_called_once_with() + set_pending_boot_mode_mock.assert_called_once_with('UEFI') + self.assertEqual('uefi', + task.node.instance_info['deploy_boot_mode']) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_update_boot_mode_unknown_except(self, + get_ilo_object_mock): + ilo_mock_obj = get_ilo_object_mock.return_value + ilo_mock_obj.get_pending_boot_mode.return_value = 'UNKNOWN' + set_pending_boot_mode_mock = ilo_mock_obj.set_pending_boot_mode + exc = ilo_error.IloError('error') + set_pending_boot_mode_mock.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + ilo_common.update_boot_mode, task) + get_ilo_object_mock.assert_called_once_with(task.node) + ilo_mock_obj.get_pending_boot_mode.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_update_boot_mode_legacy(self, + get_ilo_object_mock): + ilo_mock_obj = get_ilo_object_mock.return_value + exc = ilo_error.IloCommandNotSupportedError('error') + ilo_mock_obj.get_pending_boot_mode.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.update_boot_mode(task) + get_ilo_object_mock.assert_called_once_with(task.node) + ilo_mock_obj.get_pending_boot_mode.assert_called_once_with() + self.assertEqual('bios', + task.node.instance_info['deploy_boot_mode']) + + @mock.patch.object(ilo_common, 'set_boot_mode', spec_set=True, + autospec=True) + def test_update_boot_mode_prop_boot_mode_exist(self, + set_boot_mode_mock): + + properties = {'capabilities': 'boot_mode:uefi'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties = properties + ilo_common.update_boot_mode(task) + set_boot_mode_mock.assert_called_once_with(task.node, 'uefi') + + @mock.patch.object(images, 'get_temp_url_for_glance_image', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'attach_vmedia', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, '_prepare_floppy_image', spec_set=True, + autospec=True) + def test_setup_vmedia_for_boot_with_parameters( + self, prepare_image_mock, attach_vmedia_mock, temp_url_mock): + parameters = {'a': 'b'} + boot_iso = '733d1c44-a2ea-414b-aca7-69decf20d810' + prepare_image_mock.return_value = 'floppy_url' + temp_url_mock.return_value = 'image_url' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.setup_vmedia_for_boot(task, boot_iso, parameters) + prepare_image_mock.assert_called_once_with(task, parameters) + attach_vmedia_mock.assert_any_call(task.node, 'FLOPPY', + 'floppy_url') + + temp_url_mock.assert_called_once_with( + task.context, '733d1c44-a2ea-414b-aca7-69decf20d810') + attach_vmedia_mock.assert_any_call(task.node, 'CDROM', 'image_url') + + @mock.patch.object(swift, 'SwiftAPI', spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'attach_vmedia', spec_set=True, + autospec=True) + def test_setup_vmedia_for_boot_with_swift(self, attach_vmedia_mock, + swift_api_mock): + swift_obj_mock = swift_api_mock.return_value + boot_iso = 'swift:object-name' + swift_obj_mock.get_temp_url.return_value = 'image_url' + CONF.keystone_authtoken.auth_uri = 'http://authurl' + CONF.ilo.swift_ilo_container = 'ilo_cont' + CONF.ilo.swift_object_expiry_timeout = 1 + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.setup_vmedia_for_boot(task, boot_iso) + swift_obj_mock.get_temp_url.assert_called_once_with( + 'ilo_cont', 'object-name', 1) + attach_vmedia_mock.assert_called_once_with( + task.node, 'CDROM', 'image_url') + + @mock.patch.object(ilo_common, 'attach_vmedia', spec_set=True, + autospec=True) + def test_setup_vmedia_for_boot_with_url(self, attach_vmedia_mock): + boot_iso = 'http://abc.com/img.iso' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.setup_vmedia_for_boot(task, boot_iso) + attach_vmedia_mock.assert_called_once_with(task.node, 'CDROM', + boot_iso) + + @mock.patch.object(ilo_common, 'eject_vmedia_devices', + spec_set=True, autospec=True) + @mock.patch.object(swift, 'SwiftAPI', spec_set=True, autospec=True) + @mock.patch.object(ilo_common, '_get_floppy_image_name', spec_set=True, + autospec=True) + def test_cleanup_vmedia_boot(self, get_name_mock, swift_api_mock, + eject_mock): + swift_obj_mock = swift_api_mock.return_value + CONF.ilo.swift_ilo_container = 'ilo_cont' + + get_name_mock.return_value = 'image-node-uuid' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.cleanup_vmedia_boot(task) + swift_obj_mock.delete_object.assert_called_once_with( + 'ilo_cont', 'image-node-uuid') + eject_mock.assert_called_once_with(task) + + @mock.patch.object(ilo_common, 'eject_vmedia_devices', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'destroy_floppy_image_from_web_server', + spec_set=True, autospec=True) + def test_cleanup_vmedia_boot_for_webserver(self, + destroy_image_mock, + eject_mock): + CONF.ilo.use_web_server_for_images = True + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.cleanup_vmedia_boot(task) + destroy_image_mock.assert_called_once_with(task.node) + eject_mock.assert_called_once_with(task) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_eject_vmedia_devices(self, get_ilo_object_mock): + ilo_object_mock = mock.MagicMock(spec=['eject_virtual_media']) + get_ilo_object_mock.return_value = ilo_object_mock + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.eject_vmedia_devices(task) + + ilo_object_mock.eject_virtual_media.assert_has_calls( + [mock.call('FLOPPY'), mock.call('CDROM')]) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_eject_vmedia_devices_raises( + self, get_ilo_object_mock): + ilo_object_mock = mock.MagicMock(spec=['eject_virtual_media']) + get_ilo_object_mock.return_value = ilo_object_mock + exc = ilo_error.IloError('error') + ilo_object_mock.eject_virtual_media.side_effect = exc + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + ilo_common.eject_vmedia_devices, + task) + + ilo_object_mock.eject_virtual_media.assert_called_once_with( + 'FLOPPY') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_secure_boot_mode(self, + get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + ilo_object_mock.get_current_boot_mode.return_value = 'UEFI' + ilo_object_mock.get_secure_boot_mode.return_value = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ret = ilo_common.get_secure_boot_mode(task) + ilo_object_mock.get_current_boot_mode.assert_called_once_with() + ilo_object_mock.get_secure_boot_mode.assert_called_once_with() + self.assertTrue(ret) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_secure_boot_mode_bios(self, + get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + ilo_object_mock.get_current_boot_mode.return_value = 'BIOS' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ret = ilo_common.get_secure_boot_mode(task) + ilo_object_mock.get_current_boot_mode.assert_called_once_with() + self.assertFalse(ilo_object_mock.get_secure_boot_mode.called) + self.assertFalse(ret) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_secure_boot_mode_fail(self, + get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.get_current_boot_mode.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + ilo_common.get_secure_boot_mode, + task) + ilo_mock_object.get_current_boot_mode.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_secure_boot_mode_not_supported(self, + ilo_object_mock): + ilo_mock_object = ilo_object_mock.return_value + exc = ilo_error.IloCommandNotSupportedError('error') + ilo_mock_object.get_current_boot_mode.return_value = 'UEFI' + ilo_mock_object.get_secure_boot_mode.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationNotSupported, + ilo_common.get_secure_boot_mode, + task) + ilo_mock_object.get_current_boot_mode.assert_called_once_with() + ilo_mock_object.get_secure_boot_mode.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_secure_boot_mode(self, + get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.set_secure_boot_mode(task, True) + ilo_object_mock.set_secure_boot_mode.assert_called_once_with(True) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_secure_boot_mode_fail(self, + get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.set_secure_boot_mode.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + ilo_common.set_secure_boot_mode, + task, False) + ilo_mock_object.set_secure_boot_mode.assert_called_once_with(False) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_secure_boot_mode_not_supported(self, + ilo_object_mock): + ilo_mock_object = ilo_object_mock.return_value + exc = ilo_error.IloCommandNotSupportedError('error') + ilo_mock_object.set_secure_boot_mode.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationNotSupported, + ilo_common.set_secure_boot_mode, + task, False) + ilo_mock_object.set_secure_boot_mode.assert_called_once_with(False) + + @mock.patch.object(os, 'chmod', spec_set=True, + autospec=True) + @mock.patch.object(shutil, 'copyfile', spec_set=True, + autospec=True) + def test_copy_image_to_web_server(self, copy_mock, + chmod_mock): + CONF.deploy.http_url = "http://x.y.z.a/webserver/" + CONF.deploy.http_root = "/webserver" + expected_url = "http://x.y.z.a/webserver/image-UUID" + source = 'tmp_image_file' + destination = "image-UUID" + image_path = "/webserver/image-UUID" + actual_url = ilo_common.copy_image_to_web_server(source, destination) + self.assertEqual(expected_url, actual_url) + copy_mock.assert_called_once_with(source, image_path) + chmod_mock.assert_called_once_with(image_path, 0o644) + + @mock.patch.object(os, 'chmod', spec_set=True, + autospec=True) + @mock.patch.object(shutil, 'copyfile', spec_set=True, + autospec=True) + def test_copy_image_to_web_server_fails(self, copy_mock, + chmod_mock): + CONF.deploy.http_url = "http://x.y.z.a/webserver/" + CONF.deploy.http_root = "/webserver" + source = 'tmp_image_file' + destination = "image-UUID" + image_path = "/webserver/image-UUID" + exc = exception.ImageUploadFailed('reason') + copy_mock.side_effect = exc + self.assertRaises(exception.ImageUploadFailed, + ilo_common.copy_image_to_web_server, + source, destination) + copy_mock.assert_called_once_with(source, image_path) + self.assertFalse(chmod_mock.called) + + @mock.patch.object(utils, 'unlink_without_raise', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, '_get_floppy_image_name', spec_set=True, + autospec=True) + def test_destroy_floppy_image_from_web_server(self, get_floppy_name_mock, + utils_mock): + get_floppy_name_mock.return_value = 'image-uuid' + CONF.deploy.http_root = "/webserver/" + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.destroy_floppy_image_from_web_server(task.node) + get_floppy_name_mock.assert_called_once_with(task.node) + utils_mock.assert_called_once_with('/webserver/image-uuid') diff --git a/ironic/tests/unit/drivers/modules/ilo/test_deploy.py b/ironic/tests/unit/drivers/modules/ilo/test_deploy.py new file mode 100644 index 000000000..451761bed --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/test_deploy.py @@ -0,0 +1,1860 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# 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. + +"""Test class for common methods used by iLO modules.""" + +import tempfile + +import mock +from oslo_config import cfg +import six + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.glance_service import service_utils +from ironic.common import image_service +from ironic.common import images +from ironic.common import states +from ironic.common import swift +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import agent +from ironic.drivers.modules import agent_base_vendor +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.ilo import common as ilo_common +from ironic.drivers.modules.ilo import deploy as ilo_deploy +from ironic.drivers.modules import iscsi_deploy +from ironic.drivers import utils as driver_utils +from ironic.tests.unit.conductor import utils as mgr_utils +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 + + +if six.PY3: + import io + file = io.BytesIO + +INFO_DICT = db_utils.get_test_ilo_info() +CONF = cfg.CONF + + +class IloDeployPrivateMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloDeployPrivateMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_ilo', driver_info=INFO_DICT) + + def test__get_boot_iso_object_name(self): + boot_iso_actual = ilo_deploy._get_boot_iso_object_name(self.node) + boot_iso_expected = "boot-%s" % self.node.uuid + self.assertEqual(boot_iso_expected, boot_iso_actual) + + @mock.patch.object(image_service.HttpImageService, 'validate_href', + spec_set=True, autospec=True) + def test__get_boot_iso_http_url(self, service_mock): + url = 'http://abc.org/image/qcow2' + i_info = self.node.instance_info + i_info['ilo_boot_iso'] = url + self.node.instance_info = i_info + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + boot_iso_actual = ilo_deploy._get_boot_iso(task, 'root-uuid') + service_mock.assert_called_once_with(mock.ANY, url) + self.assertEqual(url, boot_iso_actual) + + @mock.patch.object(image_service.HttpImageService, 'validate_href', + spec_set=True, autospec=True) + def test__get_boot_iso_url(self, mock_validate): + url = 'http://aaa/bbb' + i_info = self.node.instance_info + i_info['ilo_boot_iso'] = url + self.node.instance_info = i_info + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + boot_iso_actual = ilo_deploy._get_boot_iso(task, 'root-uuid') + self.assertEqual(url, boot_iso_actual) + mock_validate.assert_called_once_with(mock.ANY, url) + + @mock.patch.object(image_service.HttpImageService, 'validate_href', + spec_set=True, autospec=True) + def test__get_boot_iso_unsupported_url(self, validate_href_mock): + validate_href_mock.side_effect = iter( + [exception.ImageRefValidationFailed( + image_href='file://img.qcow2', reason='fail')]) + url = 'file://img.qcow2' + i_info = self.node.instance_info + i_info['ilo_boot_iso'] = url + self.node.instance_info = i_info + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.ImageRefValidationFailed, + ilo_deploy._get_boot_iso, task, 'root-uuid') + + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__get_boot_iso_glance_image(self, deploy_info_mock, + image_props_mock): + deploy_info_mock.return_value = {'image_source': 'image-uuid', + 'ilo_deploy_iso': 'deploy_iso_uuid'} + image_props_mock.return_value = {'boot_iso': 'boot-iso-uuid', + 'kernel_id': None, + 'ramdisk_id': None} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + boot_iso_actual = ilo_deploy._get_boot_iso(task, 'root-uuid') + deploy_info_mock.assert_called_once_with(task.node) + image_props_mock.assert_called_once_with( + task.context, 'image-uuid', + ['boot_iso', 'kernel_id', 'ramdisk_id']) + boot_iso_expected = 'boot-iso-uuid' + self.assertEqual(boot_iso_expected, boot_iso_actual) + + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__get_boot_iso_uefi_no_glance_image(self, + deploy_info_mock, + image_props_mock, + boot_mode_mock): + deploy_info_mock.return_value = {'image_source': 'image-uuid', + 'ilo_deploy_iso': 'deploy_iso_uuid'} + image_props_mock.return_value = {'boot_iso': None, + 'kernel_id': None, + 'ramdisk_id': None} + properties = {'capabilities': 'boot_mode:uefi'} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties = properties + boot_iso_result = ilo_deploy._get_boot_iso(task, 'root-uuid') + deploy_info_mock.assert_called_once_with(task.node) + image_props_mock.assert_called_once_with( + task.context, 'image-uuid', + ['boot_iso', 'kernel_id', 'ramdisk_id']) + self.assertFalse(boot_mode_mock.called) + self.assertIsNone(boot_iso_result) + + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(swift, 'SwiftAPI', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso_object_name', spec_set=True, + autospec=True) + @mock.patch.object(driver_utils, 'get_node_capability', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__get_boot_iso_create(self, deploy_info_mock, image_props_mock, + capability_mock, boot_object_name_mock, + swift_api_mock, + create_boot_iso_mock, tempfile_mock): + CONF.ilo.swift_ilo_container = 'ilo-cont' + CONF.pxe.pxe_append_params = 'kernel-params' + + swift_obj_mock = swift_api_mock.return_value + fileobj_mock = mock.MagicMock(spec=file) + fileobj_mock.name = 'tmpfile' + mock_file_handle = mock.MagicMock(spec=file) + mock_file_handle.__enter__.return_value = fileobj_mock + tempfile_mock.return_value = mock_file_handle + + deploy_info_mock.return_value = {'image_source': 'image-uuid', + 'ilo_deploy_iso': 'deploy_iso_uuid'} + image_props_mock.return_value = {'boot_iso': None, + 'kernel_id': 'kernel_uuid', + 'ramdisk_id': 'ramdisk_uuid'} + boot_object_name_mock.return_value = 'abcdef' + create_boot_iso_mock.return_value = '/path/to/boot-iso' + capability_mock.return_value = 'uefi' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + boot_iso_actual = ilo_deploy._get_boot_iso(task, 'root-uuid') + deploy_info_mock.assert_called_once_with(task.node) + image_props_mock.assert_called_once_with( + task.context, 'image-uuid', + ['boot_iso', 'kernel_id', 'ramdisk_id']) + boot_object_name_mock.assert_called_once_with(task.node) + create_boot_iso_mock.assert_called_once_with(task.context, + 'tmpfile', + 'kernel_uuid', + 'ramdisk_uuid', + 'deploy_iso_uuid', + 'root-uuid', + 'kernel-params', + 'uefi') + swift_obj_mock.create_object.assert_called_once_with('ilo-cont', + 'abcdef', + 'tmpfile') + boot_iso_expected = 'swift:abcdef' + self.assertEqual(boot_iso_expected, boot_iso_actual) + + @mock.patch.object(ilo_common, 'copy_image_to_web_server', spec_set=True, + autospec=True) + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso_object_name', spec_set=True, + autospec=True) + @mock.patch.object(driver_utils, 'get_node_capability', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__get_boot_iso_create_use_webserver_true_ramdisk_webserver( + self, deploy_info_mock, image_props_mock, + capability_mock, boot_object_name_mock, + create_boot_iso_mock, tempfile_mock, + copy_file_mock): + CONF.ilo.swift_ilo_container = 'ilo-cont' + CONF.ilo.use_web_server_for_images = True + CONF.deploy.http_url = "http://10.10.1.30/httpboot" + CONF.deploy.http_root = "/httpboot" + CONF.pxe.pxe_append_params = 'kernel-params' + + fileobj_mock = mock.MagicMock(spec=file) + fileobj_mock.name = 'tmpfile' + mock_file_handle = mock.MagicMock(spec=file) + mock_file_handle.__enter__.return_value = fileobj_mock + tempfile_mock.return_value = mock_file_handle + + ramdisk_href = "http://10.10.1.30/httpboot/ramdisk" + kernel_href = "http://10.10.1.30/httpboot/kernel" + deploy_info_mock.return_value = {'image_source': 'image-uuid', + 'ilo_deploy_iso': 'deploy_iso_uuid'} + image_props_mock.return_value = {'boot_iso': None, + 'kernel_id': kernel_href, + 'ramdisk_id': ramdisk_href} + boot_object_name_mock.return_value = 'abcdef' + create_boot_iso_mock.return_value = '/path/to/boot-iso' + capability_mock.return_value = 'uefi' + copy_file_mock.return_value = "http://10.10.1.30/httpboot/abcdef" + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + boot_iso_actual = ilo_deploy._get_boot_iso(task, 'root-uuid') + deploy_info_mock.assert_called_once_with(task.node) + image_props_mock.assert_called_once_with( + task.context, 'image-uuid', + ['boot_iso', 'kernel_id', 'ramdisk_id']) + boot_object_name_mock.assert_called_once_with(task.node) + create_boot_iso_mock.assert_called_once_with(task.context, + 'tmpfile', + kernel_href, + ramdisk_href, + 'deploy_iso_uuid', + 'root-uuid', + 'kernel-params', + 'uefi') + boot_iso_expected = 'http://10.10.1.30/httpboot/abcdef' + self.assertEqual(boot_iso_expected, boot_iso_actual) + copy_file_mock.assert_called_once_with(fileobj_mock.name, + 'abcdef') + + @mock.patch.object(ilo_deploy, '_get_boot_iso_object_name', spec_set=True, + autospec=True) + @mock.patch.object(swift, 'SwiftAPI', spec_set=True, autospec=True) + def test__clean_up_boot_iso_for_instance(self, swift_mock, + boot_object_name_mock): + swift_obj_mock = swift_mock.return_value + CONF.ilo.swift_ilo_container = 'ilo-cont' + boot_object_name_mock.return_value = 'boot-object' + i_info = self.node.instance_info + i_info['ilo_boot_iso'] = 'swift:bootiso' + self.node.instance_info = i_info + self.node.save() + ilo_deploy._clean_up_boot_iso_for_instance(self.node) + swift_obj_mock.delete_object.assert_called_once_with('ilo-cont', + 'boot-object') + + @mock.patch.object(utils, 'unlink_without_raise', spec_set=True, + autospec=True) + def test__clean_up_boot_iso_for_instance_on_webserver(self, unlink_mock): + + CONF.ilo.use_web_server_for_images = True + CONF.deploy.http_root = "/webserver" + i_info = self.node.instance_info + i_info['ilo_boot_iso'] = 'http://x.y.z.a/webserver/boot-object' + self.node.instance_info = i_info + self.node.save() + boot_iso_path = "/webserver/boot-object" + ilo_deploy._clean_up_boot_iso_for_instance(self.node) + unlink_mock.assert_called_once_with(boot_iso_path) + + @mock.patch.object(ilo_deploy, '_get_boot_iso_object_name', spec_set=True, + autospec=True) + def test__clean_up_boot_iso_for_instance_no_boot_iso( + self, boot_object_name_mock): + ilo_deploy._clean_up_boot_iso_for_instance(self.node) + self.assertFalse(boot_object_name_mock.called) + + @mock.patch.object(deploy_utils, 'check_for_missing_params', spec_set=True, + autospec=True) + def test__parse_driver_info(self, check_params_mock): + self.node.driver_info['ilo_deploy_iso'] = 'deploy-iso-uuid' + driver_info_expected = {'ilo_deploy_iso': 'deploy-iso-uuid'} + driver_info_actual = ilo_deploy._parse_driver_info(self.node) + error_msg = ("Error validating iLO virtual media deploy. Some" + " parameters were missing in node's driver_info") + check_params_mock.assert_called_once_with(driver_info_expected, + error_msg) + self.assertEqual(driver_info_expected, driver_info_actual) + + @mock.patch.object(ilo_deploy, '_parse_driver_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'parse_instance_info', spec_set=True, + autospec=True) + def test__parse_deploy_info(self, instance_info_mock, driver_info_mock): + instance_info_mock.return_value = {'a': 'b'} + driver_info_mock.return_value = {'c': 'd'} + expected_info = {'a': 'b', 'c': 'd'} + actual_info = ilo_deploy._parse_deploy_info(self.node) + self.assertEqual(expected_info, actual_info) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + def test__reboot_into(self, setup_vmedia_mock, set_boot_device_mock, + node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + opts = {'a': 'b'} + ilo_deploy._reboot_into(task, 'iso', opts) + setup_vmedia_mock.assert_called_once_with(task, 'iso', opts) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.CDROM) + node_power_action_mock.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(ilo_common, 'eject_vmedia_devices', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_reboot_into', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'build_agent_options', spec_set=True, + autospec=True) + def test__prepare_agent_vmedia_boot(self, build_options_mock, + reboot_into_mock, eject_mock): + deploy_opts = {'a': 'b'} + build_options_mock.return_value = deploy_opts + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_info['ilo_deploy_iso'] = 'deploy-iso-uuid' + + ilo_deploy._prepare_agent_vmedia_boot(task) + + eject_mock.assert_called_once_with(task) + build_options_mock.assert_called_once_with(task.node) + reboot_into_mock.assert_called_once_with(task, + 'deploy-iso-uuid', + deploy_opts) + + @mock.patch.object(deploy_utils, 'is_secure_boot_requested', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + def test__update_secure_boot_mode_passed_true(self, + func_set_secure_boot_mode, + func_is_secure_boot_req): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_is_secure_boot_req.return_value = True + ilo_deploy._update_secure_boot_mode(task, True) + func_set_secure_boot_mode.assert_called_once_with(task, True) + + @mock.patch.object(deploy_utils, 'is_secure_boot_requested', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + def test__update_secure_boot_mode_passed_False(self, + func_set_secure_boot_mode, + func_is_secure_boot_req): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_is_secure_boot_req.return_value = False + ilo_deploy._update_secure_boot_mode(task, False) + self.assertFalse(func_set_secure_boot_mode.called) + + @mock.patch.object(ilo_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'get_secure_boot_mode', spec_set=True, + autospec=True) + def test__disable_secure_boot_false(self, + func_get_secure_boot_mode, + func_set_secure_boot_mode): + func_get_secure_boot_mode.return_value = False + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = ilo_deploy._disable_secure_boot(task) + func_get_secure_boot_mode.assert_called_once_with(task) + self.assertFalse(func_set_secure_boot_mode.called) + self.assertFalse(returned_state) + + @mock.patch.object(ilo_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'get_secure_boot_mode', spec_set=True, + autospec=True) + def test__disable_secure_boot_true(self, + func_get_secure_boot_mode, + func_set_secure_boot_mode): + func_get_secure_boot_mode.return_value = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = ilo_deploy._disable_secure_boot(task) + func_get_secure_boot_mode.assert_called_once_with(task) + func_set_secure_boot_mode.assert_called_once_with(task, False) + self.assertTrue(returned_state) + + @mock.patch.object(ilo_deploy.LOG, 'debug', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, 'exception', spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'get_secure_boot_mode', spec_set=True, + autospec=True) + def test__disable_secure_boot_exception(self, + func_get_secure_boot_mode, + exception_mock, + mock_log): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + exception_mock.IloOperationNotSupported = Exception + func_get_secure_boot_mode.side_effect = Exception + returned_state = ilo_deploy._disable_secure_boot(task) + func_get_secure_boot_mode.assert_called_once_with(task) + self.assertTrue(mock_log.called) + self.assertFalse(returned_state) + + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_disable_secure_boot', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test__prepare_node_for_deploy(self, + func_node_power_action, + func_disable_secure_boot, + func_update_boot_mode): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_disable_secure_boot.return_value = False + ilo_deploy._prepare_node_for_deploy(task) + func_node_power_action.assert_called_once_with(task, + states.POWER_OFF) + func_disable_secure_boot.assert_called_once_with(task) + func_update_boot_mode.assert_called_once_with(task) + bootmode = driver_utils.get_node_capability(task.node, "boot_mode") + self.assertIsNone(bootmode) + + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_disable_secure_boot', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test__prepare_node_for_deploy_sec_boot_on(self, + func_node_power_action, + func_disable_secure_boot, + func_update_boot_mode): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_disable_secure_boot.return_value = True + ilo_deploy._prepare_node_for_deploy(task) + func_node_power_action.assert_called_once_with(task, + states.POWER_OFF) + func_disable_secure_boot.assert_called_once_with(task) + self.assertFalse(func_update_boot_mode.called) + ret_boot_mode = task.node.instance_info['deploy_boot_mode'] + self.assertEqual('uefi', ret_boot_mode) + bootmode = driver_utils.get_node_capability(task.node, "boot_mode") + self.assertIsNone(bootmode) + + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_disable_secure_boot', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test__prepare_node_for_deploy_inst_info(self, + func_node_power_action, + func_disable_secure_boot, + func_update_boot_mode): + instance_info = {'capabilities': '{"secure_boot": "true"}'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_disable_secure_boot.return_value = False + task.node.instance_info = instance_info + ilo_deploy._prepare_node_for_deploy(task) + func_node_power_action.assert_called_once_with(task, + states.POWER_OFF) + func_disable_secure_boot.assert_called_once_with(task) + func_update_boot_mode.assert_called_once_with(task) + bootmode = driver_utils.get_node_capability(task.node, "boot_mode") + self.assertIsNone(bootmode) + deploy_boot_mode = task.node.instance_info.get('deploy_boot_mode') + self.assertIsNone(deploy_boot_mode) + + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_disable_secure_boot', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test__prepare_node_for_deploy_sec_boot_on_inst_info( + self, func_node_power_action, func_disable_secure_boot, + func_update_boot_mode): + instance_info = {'capabilities': '{"secure_boot": "true"}'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_disable_secure_boot.return_value = True + task.node.instance_info = instance_info + ilo_deploy._prepare_node_for_deploy(task) + func_node_power_action.assert_called_once_with(task, + states.POWER_OFF) + func_disable_secure_boot.assert_called_once_with(task) + self.assertFalse(func_update_boot_mode.called) + bootmode = driver_utils.get_node_capability(task.node, "boot_mode") + self.assertIsNone(bootmode) + deploy_boot_mode = task.node.instance_info.get('deploy_boot_mode') + self.assertIsNone(deploy_boot_mode) + + @mock.patch.object(ilo_deploy.LOG, 'warning', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', spec_set=True, + autospec=True) + def test__recreate_and_populate_boot_iso_root_uuid_set(self, + get_boot_iso_mock, + log_mock): + driver_internal_info = {} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + driver_internal_info['root_uuid_or_disk_id'] = 'root-uuid' + task.node.driver_internal_info = driver_internal_info + r_uuid = task.node.driver_internal_info['root_uuid_or_disk_id'] + get_boot_iso_mock.return_value = 'boot-uuid' + ilo_deploy._recreate_and_populate_ilo_boot_iso(task) + self.assertEqual(task.node.instance_info['ilo_boot_iso'], + 'boot-uuid') + get_boot_iso_mock.assert_called_once_with(task, r_uuid) + self.assertFalse(log_mock.called) + + @mock.patch.object(ilo_deploy.LOG, 'warning', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', spec_set=True, + autospec=True) + def test__recreate_and_populate_boot_iso_root_not_set(self, + get_boot_iso_mock, + log_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['ilo_boot_iso'] = 'boot-uuid-old-iso' + ilo_deploy._recreate_and_populate_ilo_boot_iso(task) + self.assertEqual(task.node.instance_info['ilo_boot_iso'], + 'boot-uuid-old-iso') + self.assertFalse(get_boot_iso_mock.called) + self.assertTrue(log_mock.called) + + @mock.patch.object(ilo_deploy.LOG, 'warning', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', + spec_set=True, autospec=True) + def test__recreate_and_populate_get_boot_iso_fails(self, + get_boot_iso_mock, + log_mock): + driver_internal_info = {} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + driver_internal_info['boot_iso_created_in_web_server'] = True + driver_internal_info['root_uuid_or_disk_id'] = 'uuid' + task.node.instance_info['ilo_boot_iso'] = 'boot-uuid-old-iso' + task.node.driver_internal_info = driver_internal_info + task.node.save() + r_uuid = task.node.driver_internal_info.get('root_uuid_or_disk_id') + get_boot_iso_mock.side_effect = Exception + ilo_deploy._recreate_and_populate_ilo_boot_iso(task) + self.assertEqual(task.node.instance_info['ilo_boot_iso'], + 'boot-uuid-old-iso') + get_boot_iso_mock.assert_called_once_with(task, r_uuid) + self.assertTrue(log_mock.called) + + @mock.patch.object(ilo_deploy.LOG, 'warning', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', + spec_set=True, autospec=True) + def test__recreate_and_populate_get_boot_iso_none(self, + boot_iso_mock, + log_mock): + driver_internal_info = {} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + driver_internal_info['boot_iso_created_in_web_server'] = True + driver_internal_info['root_uuid_or_disk_id'] = 'uuid' + task.node.driver_internal_info = driver_internal_info + r_uuid = task.node.driver_internal_info.get('root_uuid_or_disk_id') + task.node.instance_info['ilo_boot_iso'] = 'boot-uuid-old-iso' + task.node.save() + boot_iso_mock.return_value = None + ilo_deploy._recreate_and_populate_ilo_boot_iso(task) + boot_iso_mock.assert_called_once_with(task, r_uuid) + self.assertEqual(task.node.instance_info['ilo_boot_iso'], + 'boot-uuid-old-iso') + self.assertTrue(log_mock.called) + + +class IloVirtualMediaIscsiDeployTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloVirtualMediaIscsiDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_ilo', driver_info=INFO_DICT) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + def _test_validate(self, validate_mock, + deploy_info_mock, + validate_prop_mock, + validate_capability_mock, + props_expected): + d_info = {'image_source': 'uuid'} + deploy_info_mock.return_value = d_info + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + validate_prop_mock.assert_called_once_with( + task.context, d_info, props_expected) + validate_capability_mock.assert_called_once_with(task.node) + + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + def test_validate_invalid_boot_option(self, + validate_mock, + deploy_info_mock, + validate_prop_mock): + d_info = {'image_source': '733d1c44-a2ea-414b-aca7-69decf20d810'} + properties = {'capabilities': 'boot_mode:uefi,boot_option:foo'} + deploy_info_mock.return_value = d_info + props = ['kernel_id', 'ramdisk_id'] + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties = properties + exc = self.assertRaises(exception.InvalidParameterValue, + task.driver.deploy.validate, + task) + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + validate_prop_mock.assert_called_once_with(task.context, + d_info, props) + self.assertIn('boot_option', str(exc)) + + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + def test_validate_invalid_boot_mode(self, + validate_mock, + deploy_info_mock, + validate_prop_mock): + d_info = {'image_source': '733d1c44-a2ea-414b-aca7-69decf20d810'} + properties = {'capabilities': 'boot_mode:foo,boot_option:local'} + deploy_info_mock.return_value = d_info + props = ['kernel_id', 'ramdisk_id'] + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties = properties + exc = self.assertRaises(exception.InvalidParameterValue, + task.driver.deploy.validate, + task) + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + validate_prop_mock.assert_called_once_with(task.context, + d_info, props) + self.assertIn('boot_mode', str(exc)) + + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test_validate_glance_partition_image(self, is_glance_image_mock): + is_glance_image_mock.return_value = True + self._test_validate(props_expected=['kernel_id', 'ramdisk_id']) + + def test_validate_whole_disk_image(self): + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.save() + self._test_validate(props_expected=[]) + + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test_validate_non_glance_partition_image(self, is_glance_image_mock): + is_glance_image_mock.return_value = False + self._test_validate(props_expected=['kernel', 'ramdisk']) + + @mock.patch.object(ilo_common, 'eject_vmedia_devices', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_reboot_into', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'get_single_nic_with_vif_port_id', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'build_agent_options', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'check_image_size', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'cache_instance_image', spec_set=True, + autospec=True) + def _test_deploy(self, + cache_instance_image_mock, + check_image_size_mock, + build_opts_mock, + agent_options_mock, + get_nic_mock, + reboot_into_mock, + eject_mock, + ilo_boot_iso, + image_source + ): + instance_info = self.node.instance_info + instance_info['ilo_boot_iso'] = ilo_boot_iso + instance_info['image_source'] = image_source + self.node.instance_info = instance_info + self.node.save() + + deploy_opts = {'a': 'b'} + agent_options_mock.return_value = { + 'ipa-api-url': 'http://1.2.3.4:6385'} + build_opts_mock.return_value = deploy_opts + get_nic_mock.return_value = '12:34:56:78:90:ab' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + task.node.driver_info['ilo_deploy_iso'] = 'deploy-iso' + returned_state = task.driver.deploy.deploy(task) + + eject_mock.assert_called_once_with(task) + cache_instance_image_mock.assert_called_once_with(task.context, + task.node) + check_image_size_mock.assert_called_once_with(task) + expected_ramdisk_opts = {'a': 'b', 'BOOTIF': '12:34:56:78:90:ab', + 'ipa-api-url': 'http://1.2.3.4:6385'} + build_opts_mock.assert_called_once_with(task.node) + get_nic_mock.assert_called_once_with(task) + reboot_into_mock.assert_called_once_with(task, 'deploy-iso', + expected_ramdisk_opts) + + self.assertEqual(states.DEPLOYWAIT, returned_state) + + def test_deploy_glance_image(self): + self._test_deploy( + ilo_boot_iso='swift:abcdef', + image_source='6b2f0c0c-79e8-4db6-842e-43c9764204af') + self.node.refresh() + self.assertNotIn('ilo_boot_iso', self.node.instance_info) + + def test_deploy_not_a_glance_image(self): + self._test_deploy( + ilo_boot_iso='http://mybootiso', + image_source='http://myimage') + self.node.refresh() + self.assertEqual('http://mybootiso', + self.node.instance_info['ilo_boot_iso']) + + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down(self, + node_power_action_mock, + update_secure_boot_mode_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + driver_internal_info = task.node.driver_internal_info + driver_internal_info['boot_iso_created_in_web_server'] = True + driver_internal_info['root_uuid_or_disk_id'] = 'uuid' + task.node.driver_internal_info = driver_internal_info + task.node.save() + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + self.assertEqual(states.DELETED, returned_state) + dinfo = task.node.driver_internal_info + self.assertNotIn('boot_iso_created_in_web_server', dinfo) + self.assertNotIn('root_uuid_or_disk_id', dinfo) + + @mock.patch.object(ilo_deploy.LOG, 'warn', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, 'exception', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down_handle_exception(self, + node_power_action_mock, + update_secure_boot_mode_mock, + exception_mock, + mock_log): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + exception_mock.IloOperationNotSupported = Exception + update_secure_boot_mode_mock.side_effect = Exception + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + self.assertTrue(mock_log.called) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(ilo_deploy, '_clean_up_boot_iso_for_instance', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'destroy_images', spec_set=True, + autospec=True) + def test_clean_up(self, destroy_images_mock, clean_up_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.clean_up(task) + destroy_images_mock.assert_called_once_with(task.node.uuid) + clean_up_boot_mock.assert_called_once_with(task.node) + + @mock.patch.object(ilo_deploy, '_clean_up_boot_iso_for_instance', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'destroy_floppy_image_from_web_server', + spec_set=True, autospec=True) + def test_clean_up_of_webserver_images(self, destroy_images_mock, + clean_up_boot_mock): + CONF.ilo.use_web_server_for_images = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.clean_up(task) + destroy_images_mock.assert_called_once_with(task.node) + clean_up_boot_mock.assert_called_once_with(task.node) + + @mock.patch.object(ilo_deploy, '_prepare_node_for_deploy', spec_set=True, + autospec=True) + def test_prepare(self, func_prepare_node_for_deploy): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.prepare(task) + func_prepare_node_for_deploy.assert_called_once_with(task) + + @mock.patch.object(ilo_deploy, '_prepare_node_for_deploy', spec_set=True, + autospec=True) + def test_prepare_active_node(self, func_prepare_node_for_deploy): + self.node.provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.prepare(task) + self.assertFalse(func_prepare_node_for_deploy.called) + + @mock.patch.object(ilo_deploy, '_recreate_and_populate_ilo_boot_iso', + spec_set=True, autospec=True) + def test_take_over_recreate_iso_config_and_dif_set(self, mock_recreate): + driver_internal_info = {} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + CONF.ilo.use_web_server_for_images = True + driver_internal_info['boot_iso_created_in_web_server'] = True + task.node.driver_internal_info = driver_internal_info + task.node.save() + task.driver.deploy.take_over(task) + mock_recreate.assert_called_once_with(task) + + @mock.patch.object(ilo_deploy, '_recreate_and_populate_ilo_boot_iso', + spec_set=True, autospec=True) + def test_take_over_recreate_iso_config_set_and_dif_not_set(self, + mock_recreate): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + CONF.ilo.use_web_server_for_images = True + task.node.save() + task.driver.deploy.take_over(task) + self.assertFalse(mock_recreate.called) + + @mock.patch.object(ilo_deploy, '_recreate_and_populate_ilo_boot_iso', + spec_set=True, autospec=True) + def test_take_over_recreate_iso_config_not_set(self, mock_recreate): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + CONF.ilo.use_web_server_for_images = False + task.node.save() + task.driver.deploy.take_over(task) + self.assertFalse(mock_recreate.called) + + +class IloVirtualMediaAgentDeployTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloVirtualMediaAgentDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="agent_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='agent_ilo', driver_info=INFO_DICT) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, + parse_driver_info_mock, + validate_capability_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + parse_driver_info_mock.assert_called_once_with(task.node) + validate_capability_mock.assert_called_once_with(task.node) + + @mock.patch.object(ilo_deploy, '_prepare_agent_vmedia_boot', spec_set=True, + autospec=True) + def test_deploy(self, vmedia_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.deploy(task) + vmedia_boot_mock.assert_called_once_with(task) + self.assertEqual(states.DEPLOYWAIT, returned_state) + + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down(self, + node_power_action_mock, + update_secure_boot_mode_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(ilo_deploy.LOG, 'warn', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, 'exception', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down_handle_exception(self, + node_power_action_mock, + update_secure_boot_mode_mock, + exception_mock, + mock_log): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + exception_mock.IloOperationNotSupported = Exception + update_secure_boot_mode_mock.side_effect = Exception + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + self.assertTrue(mock_log.called) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(ilo_deploy, '_prepare_node_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent, 'build_instance_info_for_deploy', spec_set=True, + autospec=True) + def test_prepare(self, + build_instance_info_mock, + func_prepare_node_for_deploy): + deploy_opts = {'a': 'b'} + build_instance_info_mock.return_value = deploy_opts + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.prepare(task) + self.assertEqual(deploy_opts, task.node.instance_info) + func_prepare_node_for_deploy.assert_called_once_with(task) + + @mock.patch.object(ilo_deploy, '_prepare_node_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent, 'build_instance_info_for_deploy', spec_set=True, + autospec=True) + def test_prepare_active_node(self, + build_instance_info_mock, + func_prepare_node_for_deploy): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.ACTIVE + task.driver.deploy.prepare(task) + self.assertFalse(build_instance_info_mock.called) + self.assertFalse(func_prepare_node_for_deploy.called) + + @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.delete_cleaning_ports', + spec_set=True, autospec=True) + @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.create_cleaning_ports', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_prepare_agent_vmedia_boot', spec_set=True, + autospec=True) + def test_prepare_cleaning(self, vmedia_boot_mock, create_port_mock, + delete_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.prepare_cleaning(task) + vmedia_boot_mock.assert_called_once_with(task) + self.assertEqual(states.CLEANWAIT, returned_state) + create_port_mock.assert_called_once_with(mock.ANY, task) + delete_mock.assert_called_once_with(mock.ANY, task) + self.assertEqual(task.node.driver_internal_info.get( + 'agent_erase_devices_iterations'), 1) + + @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.delete_cleaning_ports', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down_cleaning(self, power_mock, delete_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + task.driver.deploy.tear_down_cleaning(task) + power_mock.assert_called_once_with(task, states.POWER_OFF) + delete_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(deploy_utils, 'agent_execute_clean_step', spec_set=True, + autospec=True) + def test_execute_clean_step(self, execute_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + task.driver.deploy.execute_clean_step(task, 'fake-step') + execute_mock.assert_called_once_with(task, 'fake-step') + + @mock.patch.object(deploy_utils, 'agent_get_clean_steps', spec_set=True, + autospec=True) + def test_get_clean_steps_with_conf_option(self, get_clean_step_mock): + self.config(clean_priority_erase_devices=20, group='ilo') + get_clean_step_mock.return_value = [{ + 'step': 'erase_devices', + 'priority': 10, + 'interface': 'deploy', + 'reboot_requested': False + }] + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + step = task.driver.deploy.get_clean_steps(task) + get_clean_step_mock.assert_called_once_with(task) + self.assertEqual(step[0].get('priority'), + CONF.ilo.clean_priority_erase_devices) + + @mock.patch.object(deploy_utils, 'agent_get_clean_steps', spec_set=True, + autospec=True) + def test_get_clean_steps_erase_devices_disable(self, get_clean_step_mock): + self.config(clean_priority_erase_devices=0, group='ilo') + get_clean_step_mock.return_value = [{ + 'step': 'erase_devices', + 'priority': 10, + 'interface': 'deploy', + 'reboot_requested': False + }] + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + step = task.driver.deploy.get_clean_steps(task) + get_clean_step_mock.assert_called_once_with(task) + self.assertEqual(step[0].get('priority'), + CONF.ilo.clean_priority_erase_devices) + + @mock.patch.object(deploy_utils, 'agent_get_clean_steps', spec_set=True, + autospec=True) + def test_get_clean_steps_without_conf_option(self, get_clean_step_mock): + get_clean_step_mock.return_value = [{ + 'step': 'erase_devices', + 'priority': 10, + 'interface': 'deploy', + 'reboot_requested': False + }] + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + step = task.driver.deploy.get_clean_steps(task) + get_clean_step_mock.assert_called_once_with(task) + self.assertEqual(step[0].get('priority'), 10) + + +class VendorPassthruTestCase(db_base.DbTestCase): + + def setUp(self): + super(VendorPassthruTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_ilo") + self.node = obj_utils.create_test_node(self.context, + driver='iscsi_ilo', + driver_info=INFO_DICT) + + @mock.patch.object(iscsi_deploy, 'get_deploy_info', spec_set=True, + autospec=True) + def test_validate_pass_deploy_info(self, get_deploy_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = ilo_deploy.VendorPassthru() + vendor.validate(task, method='pass_deploy_info', foo='bar') + get_deploy_info_mock.assert_called_once_with(task.node, + foo='bar') + + @mock.patch.object(iscsi_deploy, 'validate_pass_bootloader_info_input', + spec_set=True, autospec=True) + def test_validate_pass_bootloader_install_info(self, + validate_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + kwargs = {'address': '1.2.3.4', 'key': 'fake-key', + 'status': 'SUCCEEDED', 'error': ''} + task.driver.vendor.validate( + task, method='pass_bootloader_install_info', **kwargs) + validate_mock.assert_called_once_with(task, kwargs) + + @mock.patch.object(iscsi_deploy, 'get_deploy_info', spec_set=True, + autospec=True) + def test_validate_heartbeat(self, get_deploy_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = ilo_deploy.VendorPassthru() + vendor.validate(task, method='heartbeat', foo='bar') + self.assertFalse(get_deploy_info_mock.called) + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', spec_set=True, + autospec=True) + def test__configure_vmedia_boot_with_boot_iso( + self, get_boot_iso_mock, setup_vmedia_mock, set_boot_device_mock): + root_uuid = {'root uuid': 'root_uuid'} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + get_boot_iso_mock.return_value = 'boot.iso' + + task.driver.vendor._configure_vmedia_boot( + task, root_uuid) + + get_boot_iso_mock.assert_called_once_with( + task, root_uuid) + setup_vmedia_mock.assert_called_once_with( + task, 'boot.iso') + set_boot_device_mock.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + self.assertEqual('boot.iso', + task.node.instance_info['ilo_boot_iso']) + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', spec_set=True, + autospec=True) + def test__configure_vmedia_boot_without_boot_iso( + self, get_boot_iso_mock, setup_vmedia_mock, set_boot_device_mock): + root_uuid = {'root uuid': 'root_uuid'} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + get_boot_iso_mock.return_value = None + + task.driver.vendor._configure_vmedia_boot( + task, root_uuid) + + get_boot_iso_mock.assert_called_once_with( + task, root_uuid) + self.assertFalse(setup_vmedia_mock.called) + self.assertFalse(set_boot_device_mock.called) + + @mock.patch.object(iscsi_deploy, 'validate_bootloader_install_status', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'finish_deploy', spec_set=True, + autospec=True) + def test_pass_bootloader_install_info(self, finish_deploy_mock, + validate_input_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.pass_bootloader_install_info(task, **kwargs) + finish_deploy_mock.assert_called_once_with(task, '123456') + validate_input_mock.assert_called_once_with(task, kwargs) + + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_good(self, cleanup_vmedia_boot_mock, + continue_deploy_mock, get_boot_iso_mock, + setup_vmedia_mock, set_boot_device_mock, + func_update_boot_mode, + func_update_secure_boot_mode, + notify_ramdisk_to_proceed_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': 'root-uuid'} + get_boot_iso_mock.return_value = 'boot-iso' + + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.pass_deploy_info(task, **kwargs) + + cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + get_boot_iso_mock.assert_called_once_with(task, 'root-uuid') + setup_vmedia_mock.assert_called_once_with(task, 'boot-iso') + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.CDROM, + persistent=True) + func_update_boot_mode.assert_called_once_with(task) + func_update_secure_boot_mode.assert_called_once_with(task, True) + + self.assertEqual('boot-iso', + task.node.instance_info['ilo_boot_iso']) + info = task.node.driver_internal_info['root_uuid_or_disk_id'] + self.assertEqual('root-uuid', info) + notify_ramdisk_to_proceed_mock.assert_called_once_with('123456') + + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_bad(self, cleanup_vmedia_boot_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + + self.node.provision_state = states.AVAILABLE + self.node.target_provision_state = states.NOSTATE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = task.driver.vendor + self.assertRaises(exception.InvalidState, + vendor.pass_deploy_info, + task, **kwargs) + self.assertEqual(states.AVAILABLE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + self.assertFalse(cleanup_vmedia_boot_mock.called) + + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_get_boot_iso', spec_set=True, + autospec=True) + def test_pass_deploy_info_create_boot_iso_fail( + self, get_iso_mock, cleanup_vmedia_boot_mock, continue_deploy_mock, + node_power_mock, update_boot_mode_mock, + update_secure_boot_mode_mock): + kwargs = {'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': 'root-uuid'} + get_iso_mock.side_effect = iter([exception.ImageCreationFailed( + image_type='iso', error="error")]) + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.pass_deploy_info(task, **kwargs) + + cleanup_vmedia_boot_mock.assert_called_once_with(task) + update_boot_mode_mock.assert_called_once_with(task) + update_secure_boot_mode_mock.assert_called_once_with(task, True) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + get_iso_mock.assert_called_once_with(task, 'root-uuid') + node_power_mock.assert_called_once_with(task, states.POWER_OFF) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertIsNotNone(task.node.last_error) + + @mock.patch.object(iscsi_deploy, 'finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_boot_option_local( + self, cleanup_vmedia_boot_mock, continue_deploy_mock, + func_update_boot_mode, func_update_secure_boot_mode, + set_boot_device_mock, notify_ramdisk_to_proceed_mock, + finish_deploy_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': '<some-uuid>'} + + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = task.driver.vendor + vendor.pass_deploy_info(task, **kwargs) + + cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + func_update_boot_mode.assert_called_once_with(task) + func_update_secure_boot_mode.assert_called_once_with(task, True) + notify_ramdisk_to_proceed_mock.assert_called_once_with('123456') + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertFalse(finish_deploy_mock.called) + + @mock.patch.object(iscsi_deploy, 'finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def _test_pass_deploy_info_whole_disk_image( + self, cleanup_vmedia_boot_mock, continue_deploy_mock, + func_update_boot_mode, func_update_secure_boot_mode, + set_boot_device_mock, notify_ramdisk_to_proceed_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': '<some-uuid>'} + + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = task.driver.vendor + vendor.pass_deploy_info(task, **kwargs) + + cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + func_update_boot_mode.assert_called_once_with(task) + func_update_secure_boot_mode.assert_called_once_with(task, True) + iscsi_deploy.finish_deploy.assert_called_once_with(task, '123456') + + def test_pass_deploy_info_whole_disk_image_local(self): + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + self.node.save() + self._test_pass_deploy_info_whole_disk_image() + + def test_pass_deploy_info_whole_disk_image(self): + self._test_pass_deploy_info_whole_disk_image() + + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy.VendorPassthru, '_configure_vmedia_boot', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_continue_deploy_netboot(self, cleanup_vmedia_boot_mock, + do_agent_iscsi_deploy_mock, + configure_vmedia_boot_mock, + reboot_and_finish_deploy_mock, + boot_mode_cap_mock, + update_secure_boot_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'root uuid': 'some-root-uuid'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + configure_vmedia_boot_mock.assert_called_once_with( + mock.ANY, task, 'some-root-uuid') + boot_mode_cap_mock.assert_called_once_with(task) + update_secure_boot_mock.assert_called_once_with(task, True) + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, task) + + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_continue_deploy_localboot(self, cleanup_vmedia_boot_mock, + do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + reboot_and_finish_deploy_mock, + boot_mode_cap_mock, + update_secure_boot_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.instance_info = { + 'capabilities': {'boot_option': 'local'}} + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'root uuid': 'some-root-uuid'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + configure_local_boot_mock.assert_called_once_with( + mock.ANY, task, root_uuid='some-root-uuid', + efi_system_part_uuid=None) + boot_mode_cap_mock.assert_called_once_with(task) + update_secure_boot_mock.assert_called_once_with(task, True) + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, task) + + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_continue_deploy_whole_disk_image( + self, cleanup_vmedia_boot_mock, do_agent_iscsi_deploy_mock, + configure_local_boot_mock, reboot_and_finish_deploy_mock, + boot_mode_cap_mock, update_secure_boot_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'disk identifier': 'some-disk-id'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + configure_local_boot_mock.assert_called_once_with( + mock.ANY, task, root_uuid=None, efi_system_part_uuid=None) + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, task) + + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_continue_deploy_localboot_uefi(self, cleanup_vmedia_boot_mock, + do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + reboot_and_finish_deploy_mock, + boot_mode_cap_mock, + update_secure_boot_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.instance_info = { + 'capabilities': {'boot_option': 'local'}} + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'root uuid': 'some-root-uuid', + 'efi system partition uuid': 'efi-system-part-uuid'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + configure_local_boot_mock.assert_called_once_with( + mock.ANY, task, root_uuid='some-root-uuid', + efi_system_part_uuid='efi-system-part-uuid') + boot_mode_cap_mock.assert_called_once_with(task) + update_secure_boot_mock.assert_called_once_with(task, True) + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, task) + + @mock.patch.object(ilo_deploy, '_reboot_into', spec_set=True, + autospec=True) + def test_boot_into_iso(self, reboot_into_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.boot_into_iso(task, boot_iso_href='foo') + reboot_into_mock.assert_called_once_with(task, 'foo', + ramdisk_options=None) + + @mock.patch.object(ilo_deploy.VendorPassthru, '_validate_boot_into_iso', + spec_set=True, autospec=True) + def test_validate_boot_into_iso(self, validate_boot_into_iso_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = ilo_deploy.VendorPassthru() + vendor.validate(task, method='boot_into_iso', foo='bar') + validate_boot_into_iso_mock.assert_called_once_with( + vendor, task, {'foo': 'bar'}) + + def test__validate_boot_into_iso_invalid_state(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.AVAILABLE + self.assertRaises( + exception.InvalidStateRequested, + task.driver.vendor._validate_boot_into_iso, + task, {}) + + def test__validate_boot_into_iso_missing_boot_iso_href(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.MANAGEABLE + self.assertRaises( + exception.MissingParameterValue, + task.driver.vendor._validate_boot_into_iso, + task, {}) + + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + def test__validate_boot_into_iso_manage(self, validate_image_prop_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + info = {'boot_iso_href': 'foo'} + task.node.provision_state = states.MANAGEABLE + task.driver.vendor._validate_boot_into_iso( + task, info) + validate_image_prop_mock.assert_called_once_with( + task.context, {'image_source': 'foo'}, []) + + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + def test__validate_boot_into_iso_maintenance( + self, validate_image_prop_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + info = {'boot_iso_href': 'foo'} + task.node.maintenance = True + task.driver.vendor._validate_boot_into_iso( + task, info) + validate_image_prop_mock.assert_called_once_with( + task.context, {'image_source': 'foo'}, []) + + +class IloPXEDeployTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloPXEDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="pxe_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='pxe_ilo', driver_info=INFO_DICT) + + @mock.patch.object(iscsi_deploy.ISCSIDeploy, 'validate', spec_set=True, + autospec=True) + def test_validate(self, pxe_validate_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + pxe_validate_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(iscsi_deploy.ISCSIDeploy, 'prepare', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_prepare_node_for_deploy', spec_set=True, + autospec=True) + def test_prepare(self, + prepare_node_mock, + pxe_prepare_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties['capabilities'] = 'boot_mode:uefi' + task.driver.deploy.prepare(task) + prepare_node_mock.assert_called_once_with(task) + pxe_prepare_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(iscsi_deploy.ISCSIDeploy, 'prepare', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_prepare_node_for_deploy', spec_set=True, + autospec=True) + def test_prepare_active_node(self, + prepare_node_mock, + pxe_prepare_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.ACTIVE + task.node.properties['capabilities'] = 'boot_mode:uefi' + task.driver.deploy.prepare(task) + self.assertFalse(prepare_node_mock.called) + pxe_prepare_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(iscsi_deploy.ISCSIDeploy, 'prepare', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_prepare_node_for_deploy', spec_set=True, + autospec=True) + def test_prepare_uefi_whole_disk_image_fail(self, + prepare_node_for_deploy_mock, + pxe_prepare_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties['capabilities'] = 'boot_mode:uefi' + task.node.driver_internal_info['is_whole_disk_image'] = True + self.assertRaises(exception.InvalidParameterValue, + task.driver.deploy.prepare, task) + prepare_node_for_deploy_mock.assert_called_once_with(task) + self.assertFalse(pxe_prepare_mock.called) + + @mock.patch.object(iscsi_deploy.ISCSIDeploy, 'deploy', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + def test_deploy_boot_mode_exists(self, set_persistent_mock, + pxe_deploy_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.deploy(task) + set_persistent_mock.assert_called_with(task, boot_devices.PXE) + pxe_deploy_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(iscsi_deploy.ISCSIDeploy, 'tear_down', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down(self, node_power_action_mock, + update_secure_boot_mode_mock, pxe_tear_down_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + pxe_tear_down_mock.return_value = states.DELETED + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + pxe_tear_down_mock.assert_called_once_with(mock.ANY, task) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(ilo_deploy.LOG, 'warn', spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy.ISCSIDeploy, 'tear_down', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, 'exception', spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down_handle_exception(self, node_power_action_mock, + update_secure_boot_mode_mock, + exception_mock, pxe_tear_down_mock, + mock_log): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + pxe_tear_down_mock.return_value = states.DELETED + exception_mock.IloOperationNotSupported = Exception + update_secure_boot_mode_mock.side_effect = Exception + returned_state = task.driver.deploy.tear_down(task) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + pxe_tear_down_mock.assert_called_once_with(mock.ANY, task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + self.assertTrue(mock_log.called) + self.assertEqual(states.DELETED, returned_state) + + +class IloPXEVendorPassthruTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloPXEVendorPassthruTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="pxe_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='pxe_ilo', driver_info=INFO_DICT) + + def test_vendor_routes(self): + expected = ['heartbeat', 'pass_deploy_info', + 'pass_bootloader_install_info'] + 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(list(vendor_routes))) + + def test_driver_routes(self): + expected = ['lookup'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + driver_routes = task.driver.vendor.driver_routes + self.assertIsInstance(driver_routes, dict) + self.assertEqual(sorted(expected), sorted(list(driver_routes))) + + @mock.patch.object(iscsi_deploy.VendorPassthru, 'pass_deploy_info', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + def test_vendorpassthru_pass_deploy_info(self, set_boot_device_mock, + func_update_boot_mode, + func_update_secure_boot_mode, + pxe_vendorpassthru_mock): + kwargs = {'address': '123456'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYWAIT + task.node.target_provision_state = states.ACTIVE + task.driver.vendor.pass_deploy_info(task, **kwargs) + set_boot_device_mock.assert_called_with(task, boot_devices.PXE, + persistent=True) + func_update_boot_mode.assert_called_once_with(task) + func_update_secure_boot_mode.assert_called_once_with(task, True) + pxe_vendorpassthru_mock.assert_called_once_with( + mock.ANY, task, **kwargs) + + @mock.patch.object(iscsi_deploy.VendorPassthru, 'continue_deploy', + spec_set=True, autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', autospec=True) + def test_vendorpassthru_continue_deploy(self, + func_update_boot_mode, + func_update_secure_boot_mode, + pxe_vendorpassthru_mock): + kwargs = {'address': '123456'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYWAIT + task.node.target_provision_state = states.ACTIVE + task.driver.vendor.continue_deploy(task, **kwargs) + func_update_boot_mode.assert_called_once_with(task) + func_update_secure_boot_mode.assert_called_once_with(task, True) + pxe_vendorpassthru_mock.assert_called_once_with( + mock.ANY, task, **kwargs) + + +class IloVirtualMediaAgentVendorInterfaceTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloVirtualMediaAgentVendorInterfaceTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="agent_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='agent_ilo', driver_info=INFO_DICT) + + @mock.patch.object(agent.AgentVendorInterface, 'reboot_to_instance', + spec_set=True, autospec=True) + @mock.patch.object(agent.AgentVendorInterface, 'check_deploy_success', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + def test_reboot_to_instance(self, func_update_secure_boot_mode, + func_update_boot_mode, + check_deploy_success_mock, + agent_reboot_to_instance_mock): + kwargs = {'address': '123456'} + check_deploy_success_mock.return_value = None + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.reboot_to_instance(task, **kwargs) + check_deploy_success_mock.assert_called_once_with( + mock.ANY, task.node) + func_update_boot_mode.assert_called_once_with(task) + func_update_secure_boot_mode.assert_called_once_with(task, True) + agent_reboot_to_instance_mock.assert_called_once_with( + mock.ANY, task, **kwargs) + + @mock.patch.object(agent.AgentVendorInterface, 'reboot_to_instance', + spec_set=True, autospec=True) + @mock.patch.object(agent.AgentVendorInterface, 'check_deploy_success', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_deploy, '_update_secure_boot_mode', spec_set=True, + autospec=True) + def test_reboot_to_instance_deploy_fail(self, func_update_secure_boot_mode, + func_update_boot_mode, + check_deploy_success_mock, + agent_reboot_to_instance_mock): + kwargs = {'address': '123456'} + check_deploy_success_mock.return_value = "Error" + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.reboot_to_instance(task, **kwargs) + check_deploy_success_mock.assert_called_once_with( + mock.ANY, task.node) + self.assertFalse(func_update_boot_mode.called) + self.assertFalse(func_update_secure_boot_mode.called) + agent_reboot_to_instance_mock.assert_called_once_with( + mock.ANY, task, **kwargs) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_inspect.py b/ironic/tests/unit/drivers/modules/ilo/test_inspect.py new file mode 100644 index 000000000..94ddbdd45 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/test_inspect.py @@ -0,0 +1,365 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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. + + +"""Test class for Management Interface used by iLO modules.""" + +import mock +from oslo_config import cfg +import six + +from ironic.common import exception +from ironic.common import states +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.conductor import utils as conductor_utils +from ironic.db import api as dbapi +from ironic.drivers.modules.ilo import common as ilo_common +from ironic.drivers.modules.ilo import inspect as ilo_inspect +from ironic.drivers.modules.ilo import power as ilo_power +from ironic.tests.unit.conductor import utils as mgr_utils +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 + + +INFO_DICT = db_utils.get_test_ilo_info() +CONF = cfg.CONF + + +class IloInspectTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloInspectTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='fake_ilo', driver_info=INFO_DICT) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + properties = ilo_common.REQUIRED_PROPERTIES.copy() + self.assertEqual(properties, + task.driver.inspect.get_properties()) + + @mock.patch.object(ilo_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, driver_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.inspect.validate(task) + driver_info_mock.assert_called_once_with(task.node) + + @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, + autospec=True) + @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + spec_set=True, autospec=True) + @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, + autospec=True) + @mock.patch.object(ilo_power.IloPower, 'get_power_state', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_inspect_essential_ok(self, get_ilo_object_mock, + power_mock, + get_essential_mock, + create_port_mock, + get_capabilities_mock): + ilo_object_mock = get_ilo_object_mock.return_value + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + capabilities = '' + result = {'properties': properties, 'macs': macs} + get_essential_mock.return_value = result + get_capabilities_mock.return_value = capabilities + power_mock.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.inspect.inspect_hardware(task) + self.assertEqual(properties, task.node.properties) + power_mock.assert_called_once_with(mock.ANY, task) + get_essential_mock.assert_called_once_with(task.node, + ilo_object_mock) + get_capabilities_mock.assert_called_once_with(task.node, + ilo_object_mock) + create_port_mock.assert_called_once_with(task.node, macs) + + @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, + autospec=True) + @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + spec_set=True, autospec=True) + @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, + autospec=True) + @mock.patch.object(conductor_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(ilo_power.IloPower, 'get_power_state', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_inspect_essential_ok_power_off(self, get_ilo_object_mock, + power_mock, + set_power_mock, + get_essential_mock, + create_port_mock, + get_capabilities_mock): + ilo_object_mock = get_ilo_object_mock.return_value + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + capabilities = '' + result = {'properties': properties, 'macs': macs} + get_essential_mock.return_value = result + get_capabilities_mock.return_value = capabilities + power_mock.return_value = states.POWER_OFF + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.inspect.inspect_hardware(task) + self.assertEqual(properties, task.node.properties) + power_mock.assert_called_once_with(mock.ANY, task) + set_power_mock.assert_any_call(task, states.POWER_ON) + get_essential_mock.assert_called_once_with(task.node, + ilo_object_mock) + get_capabilities_mock.assert_called_once_with(task.node, + ilo_object_mock) + create_port_mock.assert_called_once_with(task.node, macs) + + @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, + autospec=True) + @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + spec_set=True, autospec=True) + @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, + autospec=True) + @mock.patch.object(ilo_power.IloPower, 'get_power_state', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_inspect_essential_capabilities_ok(self, get_ilo_object_mock, + power_mock, + get_essential_mock, + create_port_mock, + get_capabilities_mock): + ilo_object_mock = get_ilo_object_mock.return_value + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + capability_str = 'BootMode:uefi' + capabilities = {'BootMode': 'uefi'} + result = {'properties': properties, 'macs': macs} + get_essential_mock.return_value = result + get_capabilities_mock.return_value = capabilities + power_mock.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.inspect.inspect_hardware(task) + expected_properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64', + 'capabilities': capability_str} + self.assertEqual(expected_properties, task.node.properties) + power_mock.assert_called_once_with(mock.ANY, task) + get_essential_mock.assert_called_once_with(task.node, + ilo_object_mock) + get_capabilities_mock.assert_called_once_with(task.node, + ilo_object_mock) + create_port_mock.assert_called_once_with(task.node, macs) + + @mock.patch.object(ilo_inspect, '_get_capabilities', spec_set=True, + autospec=True) + @mock.patch.object(ilo_inspect, '_create_ports_if_not_exist', + spec_set=True, autospec=True) + @mock.patch.object(ilo_inspect, '_get_essential_properties', spec_set=True, + autospec=True) + @mock.patch.object(ilo_power.IloPower, 'get_power_state', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_inspect_essential_capabilities_exist_ok(self, get_ilo_object_mock, + power_mock, + get_essential_mock, + create_port_mock, + get_capabilities_mock): + ilo_object_mock = get_ilo_object_mock.return_value + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64', + 'somekey': 'somevalue'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + result = {'properties': properties, 'macs': macs} + capabilities = {'BootMode': 'uefi'} + get_essential_mock.return_value = result + get_capabilities_mock.return_value = capabilities + power_mock.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties = {'capabilities': 'foo:bar'} + expected_capabilities = ('BootMode:uefi,' + 'foo:bar') + set1 = set(expected_capabilities.split(',')) + task.driver.inspect.inspect_hardware(task) + end_capabilities = task.node.properties['capabilities'] + set2 = set(end_capabilities.split(',')) + self.assertEqual(set1, set2) + expected_properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64', + 'capabilities': end_capabilities} + power_mock.assert_called_once_with(mock.ANY, task) + self.assertEqual(task.node.properties, expected_properties) + get_essential_mock.assert_called_once_with(task.node, + ilo_object_mock) + get_capabilities_mock.assert_called_once_with(task.node, + ilo_object_mock) + create_port_mock.assert_called_once_with(task.node, macs) + + +class TestInspectPrivateMethods(db_base.DbTestCase): + + def setUp(self): + super(TestInspectPrivateMethods, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='fake_ilo', driver_info=INFO_DICT) + + @mock.patch.object(ilo_inspect.LOG, 'info', spec_set=True, autospec=True) + @mock.patch.object(dbapi, 'get_instance', spec_set=True, autospec=True) + def test__create_ports_if_not_exist(self, instance_mock, log_mock): + db_obj = instance_mock.return_value + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + node_id = self.node.id + port_dict1 = {'address': 'aa:aa:aa:aa:aa:aa', 'node_id': node_id} + port_dict2 = {'address': 'bb:bb:bb:bb:bb:bb', 'node_id': node_id} + ilo_inspect._create_ports_if_not_exist(self.node, macs) + instance_mock.assert_called_once_with() + self.assertTrue(log_mock.called) + db_obj.create_port.assert_any_call(port_dict1) + db_obj.create_port.assert_any_call(port_dict2) + + @mock.patch.object(ilo_inspect.LOG, 'warn', spec_set=True, autospec=True) + @mock.patch.object(dbapi, 'get_instance', spec_set=True, autospec=True) + def test__create_ports_if_not_exist_mac_exception(self, + instance_mock, + log_mock): + dbapi_mock = instance_mock.return_value + dbapi_mock.create_port.side_effect = exception.MACAlreadyExists('f') + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + ilo_inspect._create_ports_if_not_exist(self.node, macs) + instance_mock.assert_called_once_with() + self.assertTrue(log_mock.called) + + def test__get_essential_properties_ok(self): + ilo_mock = mock.MagicMock(spec=['get_essential_properties']) + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + result = {'properties': properties, 'macs': macs} + ilo_mock.get_essential_properties.return_value = result + actual_result = ilo_inspect._get_essential_properties(self.node, + ilo_mock) + self.assertEqual(result, actual_result) + + def test__get_essential_properties_fail(self): + ilo_mock = mock.MagicMock( + spec=['get_additional_capabilities', 'get_essential_properties']) + # Missing key: cpu_arch + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa', 'Port 2': 'bb:bb:bb:bb:bb:bb'} + result = {'properties': properties, 'macs': macs} + ilo_mock.get_essential_properties.return_value = result + result = self.assertRaises(exception.HardwareInspectionFailure, + ilo_inspect._get_essential_properties, + self.node, + ilo_mock) + self.assertEqual( + six.text_type(result), + ("Failed to inspect hardware. Reason: Server didn't return the " + "key(s): cpu_arch")) + + def test__get_essential_properties_fail_invalid_format(self): + ilo_mock = mock.MagicMock( + spec=['get_additional_capabilities', 'get_essential_properties']) + # Not a dict + properties = ['memory_mb', '512', 'local_gb', '10', + 'cpus', '1'] + macs = ['aa:aa:aa:aa:aa:aa', 'bb:bb:bb:bb:bb:bb'] + capabilities = '' + result = {'properties': properties, 'macs': macs} + ilo_mock.get_essential_properties.return_value = result + ilo_mock.get_additional_capabilities.return_value = capabilities + self.assertRaises(exception.HardwareInspectionFailure, + ilo_inspect._get_essential_properties, + self.node, ilo_mock) + + def test__get_essential_properties_fail_mac_invalid_format(self): + ilo_mock = mock.MagicMock(spec=['get_essential_properties']) + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64'} + # Not a dict + macs = 'aa:aa:aa:aa:aa:aa' + result = {'properties': properties, 'macs': macs} + ilo_mock.get_essential_properties.return_value = result + self.assertRaises(exception.HardwareInspectionFailure, + ilo_inspect._get_essential_properties, + self.node, ilo_mock) + + def test__get_essential_properties_hardware_port_empty(self): + ilo_mock = mock.MagicMock( + spec=['get_additional_capabilities', 'get_essential_properties']) + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64'} + # Not a dictionary + macs = None + result = {'properties': properties, 'macs': macs} + capabilities = '' + ilo_mock.get_essential_properties.return_value = result + ilo_mock.get_additional_capabilities.return_value = capabilities + self.assertRaises(exception.HardwareInspectionFailure, + ilo_inspect._get_essential_properties, + self.node, ilo_mock) + + def test__get_essential_properties_hardware_port_not_dict(self): + ilo_mock = mock.MagicMock(spec=['get_essential_properties']) + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1', 'cpu_arch': 'x86_64'} + # Not a dict + macs = 'aa:bb:cc:dd:ee:ff' + result = {'properties': properties, 'macs': macs} + ilo_mock.get_essential_properties.return_value = result + result = self.assertRaises( + exception.HardwareInspectionFailure, + ilo_inspect._get_essential_properties, self.node, ilo_mock) + + @mock.patch.object(utils, 'get_updated_capabilities', spec_set=True, + autospec=True) + def test__get_capabilities_ok(self, capability_mock): + ilo_mock = mock.MagicMock(spec=['get_server_capabilities']) + capabilities = {'ilo_firmware_version': 'xyz'} + ilo_mock.get_server_capabilities.return_value = capabilities + cap = ilo_inspect._get_capabilities(self.node, ilo_mock) + self.assertEqual(cap, capabilities) + + def test__validate_ok(self): + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '2', 'cpu_arch': 'x86_arch'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa'} + data = {'properties': properties, 'macs': macs} + valid_keys = ilo_inspect.IloInspect.ESSENTIAL_PROPERTIES + ilo_inspect._validate(self.node, data) + self.assertEqual(sorted(set(properties)), sorted(valid_keys)) + + def test__validate_essential_keys_fail_missing_key(self): + properties = {'memory_mb': '512', 'local_gb': '10', + 'cpus': '1'} + macs = {'Port 1': 'aa:aa:aa:aa:aa:aa'} + data = {'properties': properties, 'macs': macs} + self.assertRaises(exception.HardwareInspectionFailure, + ilo_inspect._validate, self.node, data) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_management.py b/ironic/tests/unit/drivers/modules/ilo/test_management.py new file mode 100644 index 000000000..a3a9b7910 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/test_management.py @@ -0,0 +1,298 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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. + +"""Test class for Management Interface used by iLO modules.""" + +import mock +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.ilo import common as ilo_common +from ironic.drivers.modules.ilo import management as ilo_management +from ironic.drivers.modules import ipmitool +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +ilo_error = importutils.try_import('proliantutils.exception') + +INFO_DICT = db_utils.get_test_ilo_info() +CONF = cfg.CONF + + +class IloManagementTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloManagementTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ilo") + self.node = obj_utils.create_test_node( + self.context, driver='fake_ilo', driver_info=INFO_DICT) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected = ilo_management.MANAGEMENT_PROPERTIES + self.assertEqual(expected, + task.driver.management.get_properties()) + + @mock.patch.object(ilo_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, driver_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.validate(task) + driver_info_mock.assert_called_once_with(task.node) + + def test_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected = [boot_devices.PXE, boot_devices.DISK, + boot_devices.CDROM] + self.assertEqual( + sorted(expected), + sorted(task.driver.management. + get_supported_boot_devices(task))) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_boot_device_next_boot(self, get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + ilo_object_mock.get_one_time_boot.return_value = 'CDROM' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_device = boot_devices.CDROM + expected_response = {'boot_device': expected_device, + 'persistent': False} + self.assertEqual(expected_response, + task.driver.management.get_boot_device(task)) + ilo_object_mock.get_one_time_boot.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_boot_device_persistent(self, get_ilo_object_mock): + ilo_mock = get_ilo_object_mock.return_value + ilo_mock.get_one_time_boot.return_value = 'Normal' + ilo_mock.get_persistent_boot_device.return_value = 'NETWORK' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_device = boot_devices.PXE + expected_response = {'boot_device': expected_device, + 'persistent': True} + self.assertEqual(expected_response, + task.driver.management.get_boot_device(task)) + ilo_mock.get_one_time_boot.assert_called_once_with() + ilo_mock.get_persistent_boot_device.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_boot_device_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.get_one_time_boot.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + task.driver.management.get_boot_device, + task) + ilo_mock_object.get_one_time_boot.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_get_boot_device_persistent_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_one_time_boot.return_value = 'Normal' + exc = ilo_error.IloError('error') + ilo_mock_object.get_persistent_boot_device.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + task.driver.management.get_boot_device, + task) + ilo_mock_object.get_one_time_boot.assert_called_once_with() + ilo_mock_object.get_persistent_boot_device.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_boot_device_ok(self, get_ilo_object_mock): + ilo_object_mock = get_ilo_object_mock.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, boot_devices.CDROM, + False) + get_ilo_object_mock.assert_called_once_with(task.node) + ilo_object_mock.set_one_time_boot.assert_called_once_with('CDROM') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_boot_device_persistent_true(self, get_ilo_object_mock): + ilo_mock = get_ilo_object_mock.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, boot_devices.PXE, + True) + get_ilo_object_mock.assert_called_once_with(task.node) + ilo_mock.update_persistent_boot.assert_called_once_with( + ['NETWORK']) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_boot_device_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.set_one_time_boot.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + task.driver.management.set_boot_device, + task, boot_devices.PXE) + ilo_mock_object.set_one_time_boot.assert_called_once_with('NETWORK') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_boot_device_persistent_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.update_persistent_boot.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + task.driver.management.set_boot_device, + task, boot_devices.PXE, True) + ilo_mock_object.update_persistent_boot.assert_called_once_with( + ['NETWORK']) + + def test_set_boot_device_invalid_device(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.management.set_boot_device, + task, 'fake-device') + + @mock.patch.object(ilo_common, 'update_ipmi_properties', spec_set=True, + autospec=True) + @mock.patch.object(ipmitool.IPMIManagement, 'get_sensors_data', + spec_set=True, autospec=True) + def test_get_sensor_data(self, get_sensors_data_mock, update_ipmi_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.get_sensors_data(task) + update_ipmi_mock.assert_called_once_with(task) + get_sensors_data_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test__execute_ilo_clean_step_ok(self, get_ilo_object_mock): + ilo_mock = get_ilo_object_mock.return_value + clean_step_mock = getattr(ilo_mock, 'fake-step') + ilo_management._execute_ilo_clean_step( + self.node, 'fake-step', 'args', kwarg='kwarg') + clean_step_mock.assert_called_once_with('args', kwarg='kwarg') + + @mock.patch.object(ilo_management, 'LOG', spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test__execute_ilo_clean_step_not_supported(self, get_ilo_object_mock, + log_mock): + ilo_mock = get_ilo_object_mock.return_value + exc = ilo_error.IloCommandNotSupportedError("error") + clean_step_mock = getattr(ilo_mock, 'fake-step') + clean_step_mock.side_effect = exc + ilo_management._execute_ilo_clean_step( + self.node, 'fake-step', 'args', kwarg='kwarg') + clean_step_mock.assert_called_once_with('args', kwarg='kwarg') + self.assertTrue(log_mock.warn.called) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test__execute_ilo_clean_step_fail(self, get_ilo_object_mock): + ilo_mock = get_ilo_object_mock.return_value + exc = ilo_error.IloError("error") + clean_step_mock = getattr(ilo_mock, 'fake-step') + clean_step_mock.side_effect = exc + self.assertRaises(exception.NodeCleaningFailure, + ilo_management._execute_ilo_clean_step, + self.node, 'fake-step', 'args', kwarg='kwarg') + clean_step_mock.assert_called_once_with('args', kwarg='kwarg') + + @mock.patch.object(ilo_management, '_execute_ilo_clean_step', + spec_set=True, autospec=True) + def test_reset_ilo(self, clean_step_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.reset_ilo(task) + clean_step_mock.assert_called_once_with(task.node, 'reset_ilo') + + @mock.patch.object(ilo_management, '_execute_ilo_clean_step', + spec_set=True, autospec=True) + def test_reset_ilo_credential_ok(self, clean_step_mock): + info = self.node.driver_info + info['ilo_change_password'] = "fake-password" + self.node.driver_info = info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.reset_ilo_credential(task) + clean_step_mock.assert_called_once_with( + task.node, 'reset_ilo_credential', 'fake-password') + self.assertIsNone( + task.node.driver_info.get('ilo_change_password')) + self.assertEqual(task.node.driver_info['ilo_password'], + 'fake-password') + + @mock.patch.object(ilo_management, 'LOG', spec_set=True, autospec=True) + @mock.patch.object(ilo_management, '_execute_ilo_clean_step', + spec_set=True, autospec=True) + def test_reset_ilo_credential_no_password(self, clean_step_mock, + log_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.reset_ilo_credential(task) + self.assertFalse(clean_step_mock.called) + self.assertTrue(log_mock.info.called) + + @mock.patch.object(ilo_management, '_execute_ilo_clean_step', + spec_set=True, autospec=True) + def test_reset_bios_to_default(self, clean_step_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.reset_bios_to_default(task) + clean_step_mock.assert_called_once_with(task.node, + 'reset_bios_to_default') + + @mock.patch.object(ilo_management, '_execute_ilo_clean_step', + spec_set=True, autospec=True) + def test_reset_secure_boot_keys_to_default(self, clean_step_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.reset_secure_boot_keys_to_default(task) + clean_step_mock.assert_called_once_with(task.node, + 'reset_secure_boot_keys') + + @mock.patch.object(ilo_management, '_execute_ilo_clean_step', + spec_set=True, autospec=True) + def test_clear_secure_boot_keys(self, clean_step_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.clear_secure_boot_keys(task) + clean_step_mock.assert_called_once_with(task.node, + 'clear_secure_boot_keys') diff --git a/ironic/tests/unit/drivers/modules/ilo/test_power.py b/ironic/tests/unit/drivers/modules/ilo/test_power.py new file mode 100644 index 000000000..13fade795 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/test_power.py @@ -0,0 +1,231 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# 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. + +"""Test class for IloPower module.""" + +import mock +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules.ilo import common as ilo_common +from ironic.drivers.modules.ilo import power as ilo_power +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +ilo_error = importutils.try_import('proliantutils.exception') + +INFO_DICT = db_utils.get_test_ilo_info() +CONF = cfg.CONF + + +@mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, autospec=True) +class IloPowerInternalMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloPowerInternalMethodsTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver="fake_ilo") + self.node = db_utils.create_test_node( + driver='fake_ilo', + driver_info=driver_info, + instance_uuid='instance_uuid_123') + CONF.set_override('power_retry', 2, 'ilo') + CONF.set_override('power_wait', 0, 'ilo') + + def test__get_power_state(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + + self.assertEqual( + states.POWER_ON, ilo_power._get_power_state(self.node)) + + ilo_mock_object.get_host_power_status.return_value = 'OFF' + self.assertEqual( + states.POWER_OFF, ilo_power._get_power_state(self.node)) + + ilo_mock_object.get_host_power_status.return_value = 'ERROR' + self.assertEqual(states.ERROR, ilo_power._get_power_state(self.node)) + + def test__get_power_state_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.get_host_power_status.side_effect = exc + + self.assertRaises(exception.IloOperationError, + ilo_power._get_power_state, + self.node) + ilo_mock_object.get_host_power_status.assert_called_once_with() + + def test__set_power_state_invalid_state(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + ilo_power._set_power_state, + task, + states.ERROR) + + def test__set_power_state_reboot_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_mock_object.reset_server.side_effect = exc + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IloOperationError, + ilo_power._set_power_state, + task, + states.REBOOT) + ilo_mock_object.reset_server.assert_called_once_with() + + def test__set_power_state_reboot_ok(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.side_effect = ['ON', 'OFF', 'ON'] + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_power._set_power_state(task, states.REBOOT) + + ilo_mock_object.reset_server.assert_called_once_with() + + def test__set_power_state_off_fail(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.return_value = 'ON' + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.PowerStateFailure, + ilo_power._set_power_state, + task, + states.POWER_OFF) + + ilo_mock_object.get_host_power_status.assert_called_with() + ilo_mock_object.hold_pwr_btn.assert_called_once_with() + + def test__set_power_state_on_ok(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + ilo_mock_object.get_host_power_status.side_effect = ['OFF', 'ON'] + + target_state = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_power._set_power_state(task, target_state) + ilo_mock_object.get_host_power_status.assert_called_with() + ilo_mock_object.set_host_power.assert_called_once_with('ON') + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + def test__attach_boot_iso_if_needed( + self, setup_vmedia_mock, set_boot_device_mock, + get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.ACTIVE + task.node.instance_info['ilo_boot_iso'] = 'boot-iso' + ilo_power._attach_boot_iso_if_needed(task) + setup_vmedia_mock.assert_called_once_with(task, 'boot-iso') + set_boot_device_mock.assert_called_once_with(task, + boot_devices.CDROM) + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + def test__attach_boot_iso_if_needed_on_rebuild( + self, setup_vmedia_mock, set_boot_device_mock, + get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.instance_info['ilo_boot_iso'] = 'boot-iso' + ilo_power._attach_boot_iso_if_needed(task) + self.assertFalse(setup_vmedia_mock.called) + self.assertFalse(set_boot_device_mock.called) + + +class IloPowerTestCase(db_base.DbTestCase): + + def setUp(self): + super(IloPowerTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver="fake_ilo") + self.node = obj_utils.create_test_node(self.context, + driver='fake_ilo', + driver_info=driver_info) + + def test_get_properties(self): + expected = ilo_common.COMMON_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.power.get_properties()) + + @mock.patch.object(ilo_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(ilo_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate_fail(self, mock_drvinfo): + side_effect = iter([exception.InvalidParameterValue("Invalid Input")]) + mock_drvinfo.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, + task) + + @mock.patch.object(ilo_power, '_get_power_state', spec_set=True, + autospec=True) + def test_get_power_state(self, mock_get_power): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_get_power.return_value = states.POWER_ON + self.assertEqual(states.POWER_ON, + task.driver.power.get_power_state(task)) + mock_get_power.assert_called_once_with(task.node) + + @mock.patch.object(ilo_power, '_set_power_state', spec_set=True, + autospec=True) + def test_set_power_state(self, mock_set_power): + mock_set_power.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + mock_set_power.assert_called_once_with(task, states.POWER_ON) + + @mock.patch.object(ilo_power, '_set_power_state', spec_set=True, + autospec=True) + @mock.patch.object(ilo_power, '_get_power_state', spec_set=True, + autospec=True) + def test_reboot(self, mock_get_power, mock_set_power): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_get_power.return_value = states.POWER_ON + mock_set_power.return_value = states.POWER_ON + task.driver.power.reboot(task) + mock_get_power.assert_called_once_with(task.node) + mock_set_power.assert_called_once_with(task, states.REBOOT) diff --git a/ironic/tests/unit/drivers/modules/irmc/__init__.py b/ironic/tests/unit/drivers/modules/irmc/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/__init__.py diff --git a/ironic/tests/unit/drivers/modules/irmc/fake_sensors_data_ng.xml b/ironic/tests/unit/drivers/modules/irmc/fake_sensors_data_ng.xml new file mode 100644 index 000000000..c8788427b --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/fake_sensors_data_ng.xml @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet type="text/xsl" href="Report.xslt"?> +<Root Schema="2" Version="7.65F" OS="iRMC S4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <System> + <SensorDataRecords Schema="1"> + <SDR Nr="1" RecordID="1" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>1</SensorNumber> + </Key> + <Entity> + <ID>55</ID> + <Instance>0</Instance> + <Name>Ambient</Name> + </Entity> + <Sensor> + <Type>1</Type> + <!-- <TypeName>Temperature</TypeName> --> + <BaseUnit>1</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <BaseUnitName>degree C</BaseUnitName> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <UpperCritical> + <Raw>168</Raw> + <Normalized>42</Normalized> + </UpperCritical> + <LowerCritical> + <Raw>4</Raw> + <Normalized>1</Normalized> + </LowerCritical> + <UpperNonCritical> + <Raw>148</Raw> + <Normalized>37</Normalized> + </UpperNonCritical> + <LowerNonCritical> + <Raw>24</Raw> + <Normalized>6</Normalized> + </LowerNonCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + <SDR Nr="2" RecordID="2" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>2</SensorNumber> + </Key> + <Entity> + <ID>7</ID> + <Instance>0</Instance> + <Name>Systemboard 1</Name> + </Entity> + <Sensor> + <!-- <Type>1</Type> --> + <TypeName>Temperature</TypeName> + <BaseUnit>1</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <BaseUnitName>degree C</BaseUnitName> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <UpperCritical> + <Raw>80</Raw> + <Normalized>80</Normalized> + </UpperCritical> + <UpperNonCritical> + <Raw>75</Raw> + <Normalized>75</Normalized> + </UpperNonCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + <SDR Nr="34" RecordID="34" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>35</SensorNumber> + </Key> + <Entity> + <ID>29</ID> + <Instance>0</Instance> + <!-- <Name>FAN1 SYS</Name> --> + </Entity> + <Sensor> + <Type>4</Type> + <TypeName>Fan</TypeName> + <BaseUnit>18</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <BaseUnitName>RPM</BaseUnitName> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <LowerCritical> + <Raw>10</Raw> + <Normalized>600</Normalized> + </LowerCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + <SDR Nr="35" RecordID="35" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>36</SensorNumber> + </Key> + <Entity> + <!-- <ID>29</ID> --> + <Instance>1</Instance> + <Name>FAN2 SYS</Name> + </Entity> + <Sensor> + <Type>4</Type> + <TypeName>Fan</TypeName> + <BaseUnit>18</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <BaseUnitName>RPM</BaseUnitName> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <LowerCritical> + <Raw>10</Raw> + <Normalized>600</Normalized> + </LowerCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + </SensorDataRecords> + </System> +</Root> diff --git a/ironic/tests/unit/drivers/modules/irmc/fake_sensors_data_ok.xml b/ironic/tests/unit/drivers/modules/irmc/fake_sensors_data_ok.xml new file mode 100644 index 000000000..fb8edba68 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/fake_sensors_data_ok.xml @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet type="text/xsl" href="Report.xslt"?> +<Root Schema="2" Version="7.65F" OS="iRMC S4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <System> + <SensorDataRecords Schema="1"> + <SDR Nr="1" RecordID="1" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>1</SensorNumber> + </Key> + <Entity> + <ID>55</ID> + <Instance>0</Instance> + <Name>Ambient</Name> + </Entity> + <Sensor> + <Type>1</Type> + <TypeName>Temperature</TypeName> + <BaseUnit>1</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <BaseUnitName>degree C</BaseUnitName> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <UpperCritical> + <Raw>168</Raw> + <Normalized>42</Normalized> + </UpperCritical> + <LowerCritical> + <Raw>4</Raw> + <Normalized>1</Normalized> + </LowerCritical> + <UpperNonCritical> + <Raw>148</Raw> + <Normalized>37</Normalized> + </UpperNonCritical> + <LowerNonCritical> + <Raw>24</Raw> + <Normalized>6</Normalized> + </LowerNonCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + <SDR Nr="2" RecordID="2" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>2</SensorNumber> + </Key> + <Entity> + <ID>7</ID> + <Instance>0</Instance> + <Name>Systemboard 1</Name> + </Entity> + <Sensor> + <Type>1</Type> + <TypeName>Temperature</TypeName> + <BaseUnit>1</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <BaseUnitName>degree C</BaseUnitName> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <UpperCritical> + <Raw>80</Raw> + <Normalized>80</Normalized> + </UpperCritical> + <UpperNonCritical> + <Raw>75</Raw> + <Normalized>75</Normalized> + </UpperNonCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + <SDR Nr="34" RecordID="34" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>35</SensorNumber> + </Key> + <Entity> + <ID>29</ID> + <Instance>0</Instance> + <Name>FAN1 SYS</Name> + </Entity> + <Sensor> + <Type>4</Type> + <TypeName>Fan</TypeName> + <BaseUnit>18</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <BaseUnitName>RPM</BaseUnitName> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <LowerCritical> + <Raw>10</Raw> + <Normalized>600</Normalized> + </LowerCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + <SDR Nr="35" RecordID="35" RecordType="1" RecordTypeName="Full SDR" Version="1.5" Length="64"> + <Data Size="64"> + <Decoded> + <Key> + <SensorOwner Type="IPMB"> + <ID>20</ID> + <LUN>0</LUN> + <Channel>0</Channel> + </SensorOwner> + <SensorNumber>36</SensorNumber> + </Key> + <Entity> + <ID>29</ID> + <Instance>1</Instance> + <Name>FAN2 SYS</Name> + </Entity> + <Sensor> + <Type>4</Type> + <TypeName>Fan</TypeName> + <BaseUnit>18</BaseUnit> + <ModifierUnit>0</ModifierUnit> + <!-- <BaseUnitName>RPM</BaseUnitName> --> + <ModifierUnitName>unspecified</ModifierUnitName> + <Thresholds> + <LowerCritical> + <Raw>10</Raw> + <Normalized></Normalized> + </LowerCritical> + </Thresholds> + </Sensor> + </Decoded> + </Data> + </SDR> + </SensorDataRecords> + </System> +</Root> diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py new file mode 100644 index 000000000..0876e76d0 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py @@ -0,0 +1,168 @@ +# Copyright 2015 FUJITSU LIMITED +# +# 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. + +""" +Test class for common methods used by iRMC modules. +""" + +import mock + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs +from ironic.tests.unit.objects import utils as obj_utils + + +class IRMCValidateParametersTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCValidateParametersTestCase, self).setUp() + self.node = obj_utils.create_test_node( + self.context, + driver='fake_irmc', + driver_info=db_utils.get_test_irmc_info()) + + def test_parse_driver_info(self): + info = irmc_common.parse_driver_info(self.node) + + self.assertIsNotNone(info.get('irmc_address')) + self.assertIsNotNone(info.get('irmc_username')) + self.assertIsNotNone(info.get('irmc_password')) + self.assertIsNotNone(info.get('irmc_client_timeout')) + self.assertIsNotNone(info.get('irmc_port')) + self.assertIsNotNone(info.get('irmc_auth_method')) + self.assertIsNotNone(info.get('irmc_sensor_method')) + + def test_parse_driver_option_default(self): + self.node.driver_info = { + "irmc_address": "1.2.3.4", + "irmc_username": "admin0", + "irmc_password": "fake0", + } + info = irmc_common.parse_driver_info(self.node) + + self.assertEqual('basic', info.get('irmc_auth_method')) + self.assertEqual(443, info.get('irmc_port')) + self.assertEqual(60, info.get('irmc_client_timeout')) + self.assertEqual('ipmitool', info.get('irmc_sensor_method')) + + def test_parse_driver_info_missing_address(self): + del self.node.driver_info['irmc_address'] + self.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_username(self): + del self.node.driver_info['irmc_username'] + self.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_password(self): + del self.node.driver_info['irmc_password'] + self.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_timeout(self): + self.node.driver_info['irmc_client_timeout'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_port(self): + self.node.driver_info['irmc_port'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_auth_method(self): + self.node.driver_info['irmc_auth_method'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_sensor_method(self): + self.node.driver_info['irmc_sensor_method'] = 'qwe' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_multiple_params(self): + del self.node.driver_info['irmc_password'] + del self.node.driver_info['irmc_address'] + try: + irmc_common.parse_driver_info(self.node) + self.fail("parse_driver_info did not throw exception.") + except exception.MissingParameterValue as e: + self.assertIn('irmc_password', str(e)) + self.assertIn('irmc_address', str(e)) + + +class IRMCCommonMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCCommonMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_irmc") + self.info = db_utils.get_test_irmc_info() + self.node = obj_utils.create_test_node( + self.context, + driver='fake_irmc', + driver_info=self.info) + + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_client(self, mock_scci): + self.info['irmc_port'] = 80 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + mock_scci.get_client.return_value = 'get_client' + returned_mock_scci_get_client = irmc_common.get_irmc_client(self.node) + mock_scci.get_client.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_client', returned_mock_scci_get_client) + + def test_update_ipmi_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ipmi_info = { + "ipmi_address": "1.2.3.4", + "ipmi_username": "admin0", + "ipmi_password": "fake0", + } + task.node.driver_info = self.info + irmc_common.update_ipmi_properties(task) + actual_info = task.node.driver_info + expected_info = dict(self.info, **ipmi_info) + self.assertEqual(expected_info, actual_info) + + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_get_irmc_report(self, mock_scci): + self.info['irmc_port'] = 80 + self.info['irmc_auth_method'] = 'digest' + self.info['irmc_client_timeout'] = 60 + mock_scci.get_report.return_value = 'get_report' + returned_mock_scci_get_report = irmc_common.get_irmc_report(self.node) + mock_scci.get_report.assert_called_with( + self.info['irmc_address'], + self.info['irmc_username'], + self.info['irmc_password'], + port=self.info['irmc_port'], + auth_method=self.info['irmc_auth_method'], + client_timeout=self.info['irmc_client_timeout']) + self.assertEqual('get_report', returned_mock_scci_get_report) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_deploy.py b/ironic/tests/unit/drivers/modules/irmc/test_deploy.py new file mode 100644 index 000000000..bba950cbf --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_deploy.py @@ -0,0 +1,1536 @@ +# Copyright 2015 FUJITSU LIMITED +# +# 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. + +""" +Test class for iRMC Deploy Driver +""" + +import os +import shutil +import tempfile + +import mock +from oslo_config import cfg +import six + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.glance_service import service_utils +from ironic.common.i18n import _ +from ironic.common import images +from ironic.common import states +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import agent +from ironic.drivers.modules import agent_base_vendor +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import deploy as irmc_deploy +from ironic.drivers.modules import iscsi_deploy +from ironic.tests.unit.conductor import utils as mgr_utils +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 + + +if six.PY3: + import io + file = io.BytesIO + + +INFO_DICT = db_utils.get_test_irmc_info() +CONF = cfg.CONF + + +class IRMCDeployPrivateMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(IRMCDeployPrivateMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='iscsi_irmc') + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_irmc', driver_info=INFO_DICT) + + CONF.irmc.remote_image_share_root = '/remote_image_share_root' + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + CONF.irmc.remote_image_user_domain = 'local' + + @mock.patch.object(os.path, 'isdir', spec_set=True, autospec=True) + def test__parse_config_option(self, isdir_mock): + isdir_mock.return_value = True + + result = irmc_deploy._parse_config_option() + + isdir_mock.assert_called_once_with('/remote_image_share_root') + self.assertIsNone(result) + + @mock.patch.object(os.path, 'isdir', spec_set=True, autospec=True) + def test__parse_config_option_non_existed_root(self, isdir_mock): + CONF.irmc.remote_image_share_root = '/non_existed_root' + isdir_mock.return_value = False + + self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_config_option) + isdir_mock.assert_called_once_with('/non_existed_root') + + @mock.patch.object(os.path, 'isdir', spec_set=True, autospec=True) + def test__parse_config_option_wrong_share_type(self, isdir_mock): + CONF.irmc.remote_image_share_type = 'NTFS' + isdir_mock.return_value = True + + self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_config_option) + isdir_mock.assert_called_once_with('/remote_image_share_root') + + @mock.patch.object(os.path, 'isfile', spec_set=True, autospec=True) + def test__parse_driver_info_in_share(self, isfile_mock): + """With required 'irmc_deploy_iso' in share.""" + isfile_mock.return_value = True + self.node.driver_info['irmc_deploy_iso'] = 'deploy.iso' + driver_info_expected = {'irmc_deploy_iso': 'deploy.iso'} + + driver_info_actual = irmc_deploy._parse_driver_info(self.node) + + isfile_mock.assert_called_once_with( + '/remote_image_share_root/deploy.iso') + self.assertEqual(driver_info_expected, driver_info_actual) + + @mock.patch.object(service_utils, 'is_image_href_ordinary_file_name', + spec_set=True, autospec=True) + def test__parse_driver_info_not_in_share( + self, is_image_href_ordinary_file_name_mock): + """With required 'irmc_deploy_iso' not in share.""" + self.node.driver_info[ + 'irmc_deploy_iso'] = 'bc784057-a140-4130-add3-ef890457e6b3' + driver_info_expected = {'irmc_deploy_iso': + 'bc784057-a140-4130-add3-ef890457e6b3'} + is_image_href_ordinary_file_name_mock.return_value = False + + driver_info_actual = irmc_deploy._parse_driver_info(self.node) + + self.assertEqual(driver_info_expected, driver_info_actual) + + @mock.patch.object(os.path, 'isfile', spec_set=True, autospec=True) + def test__parse_driver_info_with_deploy_iso_invalid(self, isfile_mock): + """With required 'irmc_deploy_iso' non existed.""" + isfile_mock.return_value = False + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_deploy_iso'] = 'deploy.iso' + error_msg = (_("Deploy ISO file, %(deploy_iso)s, " + "not found for node: %(node)s.") % + {'deploy_iso': '/remote_image_share_root/deploy.iso', + 'node': task.node.uuid}) + + e = self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_driver_info, + task.node) + self.assertEqual(error_msg, str(e)) + + def test__parse_driver_info_with_deploy_iso_missing(self): + """With required 'irmc_deploy_iso' empty.""" + self.node.driver_info['irmc_deploy_iso'] = None + + error_msg = ("Error validating iRMC virtual media deploy. Some" + " parameters were missing in node's driver_info." + " Missing are: ['irmc_deploy_iso']") + e = self.assertRaises(exception.MissingParameterValue, + irmc_deploy._parse_driver_info, + self.node) + self.assertEqual(error_msg, str(e)) + + def test__parse_instance_info_with_boot_iso_file_name_ok(self): + """With optional 'irmc_boot_iso' file name.""" + CONF.irmc.remote_image_share_root = '/etc' + self.node.instance_info['irmc_boot_iso'] = 'hosts' + instance_info_expected = {'irmc_boot_iso': 'hosts'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_without_boot_iso_ok(self): + """With optional no 'irmc_boot_iso' file name.""" + CONF.irmc.remote_image_share_root = '/etc' + + self.node.instance_info['irmc_boot_iso'] = None + instance_info_expected = {} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_uuid_ok(self): + """With optional 'irmc_boot_iso' glance uuid.""" + self.node.instance_info[ + 'irmc_boot_iso'] = 'bc784057-a140-4130-add3-ef890457e6b3' + instance_info_expected = {'irmc_boot_iso': + 'bc784057-a140-4130-add3-ef890457e6b3'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_glance_ok(self): + """With optional 'irmc_boot_iso' glance url.""" + self.node.instance_info['irmc_boot_iso'] = ( + 'glance://bc784057-a140-4130-add3-ef890457e6b3') + instance_info_expected = { + 'irmc_boot_iso': 'glance://bc784057-a140-4130-add3-ef890457e6b3', + } + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_http_ok(self): + """With optional 'irmc_boot_iso' http url.""" + self.node.driver_info[ + 'irmc_deploy_iso'] = 'http://irmc_boot_iso' + driver_info_expected = {'irmc_deploy_iso': 'http://irmc_boot_iso'} + driver_info_actual = irmc_deploy._parse_driver_info(self.node) + + self.assertEqual(driver_info_expected, driver_info_actual) + + def test__parse_instance_info_with_boot_iso_https_ok(self): + """With optional 'irmc_boot_iso' https url.""" + self.node.instance_info[ + 'irmc_boot_iso'] = 'https://irmc_boot_iso' + instance_info_expected = {'irmc_boot_iso': 'https://irmc_boot_iso'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + def test__parse_instance_info_with_boot_iso_file_url_ok(self): + """With optional 'irmc_boot_iso' file url.""" + self.node.instance_info[ + 'irmc_boot_iso'] = 'file://irmc_boot_iso' + instance_info_expected = {'irmc_boot_iso': 'file://irmc_boot_iso'} + instance_info_actual = irmc_deploy._parse_instance_info(self.node) + + self.assertEqual(instance_info_expected, instance_info_actual) + + @mock.patch.object(os.path, 'isfile', spec_set=True, autospec=True) + def test__parse_instance_info_with_boot_iso_invalid(self, isfile_mock): + CONF.irmc.remote_image_share_root = '/etc' + isfile_mock.return_value = False + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.instance_info['irmc_boot_iso'] = 'hosts~non~existed' + + error_msg = (_("Boot ISO file, %(boot_iso)s, " + "not found for node: %(node)s.") % + {'boot_iso': '/etc/hosts~non~existed', + 'node': task.node.uuid}) + + e = self.assertRaises(exception.InvalidParameterValue, + irmc_deploy._parse_instance_info, + task.node) + self.assertEqual(error_msg, str(e)) + + @mock.patch.object(iscsi_deploy, 'parse_instance_info', spec_set=True, + autospec=True) + def test__parse_deploy_info_ok(self, instance_info_mock): + CONF.irmc.remote_image_share_root = '/etc' + instance_info_mock.return_value = {'a': 'b'} + driver_info_expected = {'a': 'b', + 'irmc_deploy_iso': 'hosts', + 'irmc_boot_iso': 'fstab'} + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_deploy_iso'] = 'hosts' + task.node.instance_info['irmc_boot_iso'] = 'fstab' + driver_info_actual = irmc_deploy._parse_deploy_info(task.node) + self.assertEqual(driver_info_expected, driver_info_actual) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + def test__reboot_into_deploy_iso_with_file(self, + fetch_mock, + setup_vmedia_mock, + set_boot_device_mock, + node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_info['irmc_deploy_iso'] = 'deploy_iso_filename' + ramdisk_opts = {'a': 'b'} + irmc_deploy._reboot_into_deploy_iso(task, ramdisk_opts) + + self.assertFalse(fetch_mock.called) + + setup_vmedia_mock.assert_called_once_with( + task, + 'deploy_iso_filename', + ramdisk_opts) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.CDROM) + node_power_action_mock.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(service_utils, 'is_image_href_ordinary_file_name', + spec_set=True, autospec=True) + def test__reboot_into_deploy_iso_with_image_service( + self, + is_image_href_ordinary_file_name_mock, + fetch_mock, + setup_vmedia_mock, + set_boot_device_mock, + node_power_action_mock): + CONF.irmc.remote_image_share_root = '/' + is_image_href_ordinary_file_name_mock.return_value = False + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_info['irmc_deploy_iso'] = 'glance://deploy_iso' + ramdisk_opts = {'a': 'b'} + irmc_deploy._reboot_into_deploy_iso(task, ramdisk_opts) + + fetch_mock.assert_called_once_with( + task.context, + 'glance://deploy_iso', + "/deploy-%s.iso" % self.node.uuid) + + setup_vmedia_mock.assert_called_once_with( + task, + "deploy-%s.iso" % self.node.uuid, + ramdisk_opts) + set_boot_device_mock.assert_called_once_with( + task, boot_devices.CDROM) + node_power_action_mock.assert_called_once_with( + task, states.REBOOT) + + def test__get_deploy_iso_name(self): + actual = irmc_deploy._get_deploy_iso_name(self.node) + expected = "deploy-%s.iso" % self.node.uuid + self.assertEqual(expected, actual) + + def test__get_boot_iso_name(self): + actual = irmc_deploy._get_boot_iso_name(self.node) + expected = "boot-%s.iso" % self.node.uuid + self.assertEqual(expected, actual) + + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__prepare_boot_iso_file(self, + deploy_info_mock, + fetch_mock, + image_props_mock, + boot_mode_mock, + create_boot_iso_mock): + deploy_info_mock.return_value = {'irmc_boot_iso': 'irmc_boot.iso'} + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_deploy._prepare_boot_iso(task, 'root-uuid') + + deploy_info_mock.assert_called_once_with(task.node) + self.assertFalse(fetch_mock.called) + self.assertFalse(image_props_mock.called) + self.assertFalse(boot_mode_mock.called) + self.assertFalse(create_boot_iso_mock.called) + task.node.refresh() + self.assertEqual('irmc_boot.iso', + task.node.driver_internal_info['irmc_boot_iso']) + + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(service_utils, 'is_image_href_ordinary_file_name', + spec_set=True, autospec=True) + def test__prepare_boot_iso_fetch_ok(self, + is_image_href_ordinary_file_name_mock, + deploy_info_mock, + fetch_mock, + image_props_mock, + boot_mode_mock, + create_boot_iso_mock): + + CONF.irmc.remote_image_share_root = '/' + image = '733d1c44-a2ea-414b-aca7-69decf20d810' + is_image_href_ordinary_file_name_mock.return_value = False + deploy_info_mock.return_value = {'irmc_boot_iso': image} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['irmc_boot_iso'] = image + irmc_deploy._prepare_boot_iso(task, 'root-uuid') + + deploy_info_mock.assert_called_once_with(task.node) + fetch_mock.assert_called_once_with( + task.context, + image, + "/boot-%s.iso" % self.node.uuid) + self.assertFalse(image_props_mock.called) + self.assertFalse(boot_mode_mock.called) + self.assertFalse(create_boot_iso_mock.called) + task.node.refresh() + self.assertEqual("boot-%s.iso" % self.node.uuid, + task.node.driver_internal_info['irmc_boot_iso']) + + @mock.patch.object(images, 'create_boot_iso', spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(images, 'get_image_properties', spec_set=True, + autospec=True) + @mock.patch.object(images, 'fetch', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + def test__prepare_boot_iso_create_ok(self, + deploy_info_mock, + fetch_mock, + image_props_mock, + boot_mode_mock, + create_boot_iso_mock): + CONF.pxe.pxe_append_params = 'kernel-params' + + deploy_info_mock.return_value = {'image_source': 'image-uuid'} + image_props_mock.return_value = {'kernel_id': 'kernel_uuid', + 'ramdisk_id': 'ramdisk_uuid'} + + CONF.irmc.remote_image_share_name = '/remote_image_share_root' + boot_mode_mock.return_value = 'uefi' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._prepare_boot_iso(task, 'root-uuid') + + self.assertFalse(fetch_mock.called) + deploy_info_mock.assert_called_once_with(task.node) + image_props_mock.assert_called_once_with( + task.context, 'image-uuid', ['kernel_id', 'ramdisk_id']) + create_boot_iso_mock.assert_called_once_with( + task.context, + '/remote_image_share_root/' + + "boot-%s.iso" % self.node.uuid, + 'kernel_uuid', 'ramdisk_uuid', + 'file:///remote_image_share_root/' + + "deploy-%s.iso" % self.node.uuid, + 'root-uuid', 'kernel-params', 'uefi') + task.node.refresh() + self.assertEqual("boot-%s.iso" % self.node.uuid, + task.node.driver_internal_info['irmc_boot_iso']) + + def test__get_floppy_image_name(self): + actual = irmc_deploy._get_floppy_image_name(self.node) + expected = "image-%s.img" % self.node.uuid + self.assertEqual(expected, actual) + + @mock.patch.object(shutil, 'copyfile', spec_set=True, autospec=True) + @mock.patch.object(images, 'create_vfat_image', spec_set=True, + autospec=True) + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + def test__prepare_floppy_image(self, + tempfile_mock, + create_vfat_image_mock, + copyfile_mock): + mock_image_file_handle = mock.MagicMock(spec=file) + mock_image_file_obj = mock.MagicMock() + mock_image_file_obj.name = 'image-tmp-file' + mock_image_file_handle.__enter__.return_value = mock_image_file_obj + tempfile_mock.side_effect = iter([mock_image_file_handle]) + + deploy_args = {'arg1': 'val1', 'arg2': 'val2'} + CONF.irmc.remote_image_share_name = '/remote_image_share_root' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._prepare_floppy_image(task, deploy_args) + + create_vfat_image_mock.assert_called_once_with( + 'image-tmp-file', parameters=deploy_args) + copyfile_mock.assert_called_once_with( + 'image-tmp-file', + '/remote_image_share_root/' + "image-%s.img" % self.node.uuid) + + @mock.patch.object(shutil, 'copyfile', spec_set=True, autospec=True) + @mock.patch.object(images, 'create_vfat_image', spec_set=True, + autospec=True) + @mock.patch.object(tempfile, 'NamedTemporaryFile', spec_set=True, + autospec=True) + def test__prepare_floppy_image_exception(self, + tempfile_mock, + create_vfat_image_mock, + copyfile_mock): + mock_image_file_handle = mock.MagicMock(spec=file) + mock_image_file_obj = mock.MagicMock() + mock_image_file_obj.name = 'image-tmp-file' + mock_image_file_handle.__enter__.return_value = mock_image_file_obj + tempfile_mock.side_effect = iter([mock_image_file_handle]) + + deploy_args = {'arg1': 'val1', 'arg2': 'val2'} + CONF.irmc.remote_image_share_name = '/remote_image_share_root' + copyfile_mock.side_effect = iter([IOError("fake error")]) + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_deploy._prepare_floppy_image, + task, + deploy_args) + + create_vfat_image_mock.assert_called_once_with( + 'image-tmp-file', parameters=deploy_args) + copyfile_mock.assert_called_once_with( + 'image-tmp-file', + '/remote_image_share_root/' + "image-%s.img" % self.node.uuid) + + @mock.patch.object(irmc_deploy, '_attach_virtual_cd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_attach_virtual_fd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_floppy_image', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_fd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_cd', spec_set=True, + autospec=True) + def test_setup_vmedia_for_boot_with_parameters(self, + _detach_virtual_cd_mock, + _detach_virtual_fd_mock, + _prepare_floppy_image_mock, + _attach_virtual_fd_mock, + _attach_virtual_cd_mock): + parameters = {'a': 'b'} + iso_filename = 'deploy_iso_or_boot_iso' + _prepare_floppy_image_mock.return_value = 'floppy_file_name' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy.setup_vmedia_for_boot(task, iso_filename, parameters) + + _detach_virtual_cd_mock.assert_called_once_with(task.node) + _detach_virtual_fd_mock.assert_called_once_with(task.node) + _prepare_floppy_image_mock.assert_called_once_with(task, + parameters) + _attach_virtual_fd_mock.assert_called_once_with(task.node, + 'floppy_file_name') + _attach_virtual_cd_mock.assert_called_once_with(task.node, + iso_filename) + + @mock.patch.object(irmc_deploy, '_attach_virtual_cd', autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_fd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_cd', spec_set=True, + autospec=True) + def test_setup_vmedia_for_boot_without_parameters( + self, + _detach_virtual_cd_mock, + _detach_virtual_fd_mock, + _attach_virtual_cd_mock): + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy.setup_vmedia_for_boot(task, 'bootable_iso_filename') + + _detach_virtual_cd_mock.assert_called_once_with(task.node) + _detach_virtual_fd_mock.assert_called_once_with(task.node) + _attach_virtual_cd_mock.assert_called_once_with( + task.node, + 'bootable_iso_filename') + + @mock.patch.object(irmc_deploy, '_get_deploy_iso_name', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_get_floppy_image_name', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_remove_share_file', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_fd', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_detach_virtual_cd', spec_set=True, + autospec=True) + def test__cleanup_vmedia_boot_ok(self, + _detach_virtual_cd_mock, + _detach_virtual_fd_mock, + _remove_share_file_mock, + _get_floppy_image_name_mock, + _get_deploy_iso_name_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._cleanup_vmedia_boot(task) + + _detach_virtual_cd_mock.assert_called_once_with(task.node) + _detach_virtual_fd_mock.assert_called_once_with(task.node) + _get_floppy_image_name_mock.assert_called_once_with(task.node) + _get_deploy_iso_name_mock.assert_called_once_with(task.node) + self.assertTrue(_remove_share_file_mock.call_count, 2) + _remove_share_file_mock.assert_has_calls( + [mock.call(_get_floppy_image_name_mock(task.node)), + mock.call(_get_deploy_iso_name_mock(task.node))]) + + @mock.patch.object(utils, 'unlink_without_raise', spec_set=True, + autospec=True) + def test__remove_share_file(self, unlink_without_raise_mock): + CONF.irmc.remote_image_share_name = '/' + + irmc_deploy._remove_share_file("boot.iso") + + unlink_without_raise_mock.assert_called_once_with('/boot.iso') + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_cd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_deploy.scci.get_virtual_cd_set_params_cmd = ( + mock.MagicMock(sepc_set=[])) + cd_set_params = (irmc_deploy.scci + .get_virtual_cd_set_params_cmd.return_value) + + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_user_domain = 'local' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + + irmc_deploy.scci.get_share_type.return_value = 0 + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._attach_virtual_cd(task.node, 'iso_filename') + + get_irmc_client_mock.assert_called_once_with(task.node) + (irmc_deploy.scci.get_virtual_cd_set_params_cmd + .assert_called_once_with)('10.20.30.40', + 'local', + 0, + 'share', + 'iso_filename', + 'admin', + 'admin0') + irmc_client.assert_has_calls( + [mock.call(cd_set_params, async=False), + mock.call(irmc_deploy.scci.MOUNT_CD, async=False)]) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_cd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._attach_virtual_cd, + task.node, + 'iso_filename') + get_irmc_client_mock.assert_called_once_with(task.node) + self.assertEqual("iRMC Inserting virtual cdrom failed. " + + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_cd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._detach_virtual_cd(task.node) + + irmc_client.assert_called_once_with(irmc_deploy.scci.UNMOUNT_CD) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_cd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._detach_virtual_cd, + task.node) + self.assertEqual("iRMC Ejecting virtual cdrom failed. " + + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_fd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_deploy.scci.get_virtual_fd_set_params_cmd = ( + mock.MagicMock(sepc_set=[])) + fd_set_params = (irmc_deploy.scci + .get_virtual_fd_set_params_cmd.return_value) + + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_user_domain = 'local' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + + irmc_deploy.scci.get_share_type.return_value = 0 + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._attach_virtual_fd(task.node, + 'floppy_image_filename') + + get_irmc_client_mock.assert_called_once_with(task.node) + (irmc_deploy.scci.get_virtual_fd_set_params_cmd + .assert_called_once_with)('10.20.30.40', + 'local', + 0, + 'share', + 'floppy_image_filename', + 'admin', + 'admin0') + irmc_client.assert_has_calls( + [mock.call(fd_set_params, async=False), + mock.call(irmc_deploy.scci.MOUNT_FD, async=False)]) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__attach_virtual_fd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._attach_virtual_fd, + task.node, + 'iso_filename') + get_irmc_client_mock.assert_called_once_with(task.node) + self.assertEqual("iRMC Inserting virtual floppy failed. " + + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_fd_ok(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_deploy._detach_virtual_fd(task.node) + + irmc_client.assert_called_once_with(irmc_deploy.scci.UNMOUNT_FD) + + @mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) + def test__detach_virtual_fd_fail(self, get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception("fake error") + irmc_deploy.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + e = self.assertRaises(exception.IRMCOperationError, + irmc_deploy._detach_virtual_fd, + task.node) + self.assertEqual("iRMC Ejecting virtual floppy failed. " + "Reason: fake error", str(e)) + + @mock.patch.object(irmc_deploy, '_parse_config_option', spec_set=True, + autospec=True) + def test__check_share_fs_mounted_ok(self, parse_conf_mock): + # Note(naohirot): mock.patch.stop() and mock.patch.start() don't work. + # therefor monkey patching is used to + # irmc_deploy._check_share_fs_mounted. + # irmc_deploy._check_share_fs_mounted is mocked in + # third_party_driver_mocks.py. + # irmc_deploy._check_share_fs_mounted_orig is the real function. + CONF.irmc.remote_image_share_root = '/' + CONF.irmc.remote_image_share_type = 'nfs' + result = irmc_deploy._check_share_fs_mounted_orig() + + parse_conf_mock.assert_called_once_with() + self.assertIsNone(result) + + @mock.patch.object(irmc_deploy, '_parse_config_option', spec_set=True, + autospec=True) + def test__check_share_fs_mounted_exception(self, parse_conf_mock): + # Note(naohirot): mock.patch.stop() and mock.patch.start() don't work. + # therefor monkey patching is used to + # irmc_deploy._check_share_fs_mounted. + # irmc_deploy._check_share_fs_mounted is mocked in + # third_party_driver_mocks.py. + # irmc_deploy._check_share_fs_mounted_orig is the real function. + CONF.irmc.remote_image_share_root = '/etc' + CONF.irmc.remote_image_share_type = 'cifs' + + self.assertRaises(exception.IRMCSharedFileSystemNotMounted, + irmc_deploy._check_share_fs_mounted_orig) + parse_conf_mock.assert_called_once_with() + + +class IRMCVirtualMediaIscsiDeployTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(IRMCVirtualMediaIscsiDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_irmc") + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_irmc', driver_info=INFO_DICT) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + @mock.patch.object(irmc_deploy, '_check_share_fs_mounted', spec_set=True, + autospec=True) + def test_validate_whole_disk_image(self, + _check_share_fs_mounted_mock, + validate_mock, + deploy_info_mock, + is_glance_image_mock, + validate_prop_mock, + validate_capabilities_mock): + d_info = {'image_source': '733d1c44-a2ea-414b-aca7-69decf20d810'} + deploy_info_mock.return_value = d_info + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_internal_info = {'is_whole_disk_image': True} + task.driver.deploy.validate(task) + + _check_share_fs_mounted_mock.assert_called_once_with() + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + self.assertFalse(is_glance_image_mock.called) + validate_prop_mock.assert_called_once_with(task.context, + d_info, []) + validate_capabilities_mock.assert_called_once_with(task.node) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + @mock.patch.object(irmc_deploy, '_check_share_fs_mounted', spec_set=True, + autospec=True) + def test_validate_glance_image(self, + _check_share_fs_mounted_mock, + validate_mock, + deploy_info_mock, + is_glance_image_mock, + validate_prop_mock, + validate_capabilities_mock): + d_info = {'image_source': '733d1c44-a2ea-414b-aca7-69decf20d810'} + deploy_info_mock.return_value = d_info + is_glance_image_mock.return_value = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + + _check_share_fs_mounted_mock.assert_called_once_with() + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + validate_prop_mock.assert_called_once_with( + task.context, d_info, ['kernel_id', 'ramdisk_id']) + validate_capabilities_mock.assert_called_once_with(task.node) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_parse_deploy_info', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'validate', spec_set=True, autospec=True) + @mock.patch.object(irmc_deploy, '_check_share_fs_mounted', spec_set=True, + autospec=True) + def test_validate_non_glance_image(self, + _check_share_fs_mounted_mock, + validate_mock, + deploy_info_mock, + is_glance_image_mock, + validate_prop_mock, + validate_capabilities_mock): + d_info = {'image_source': '733d1c44-a2ea-414b-aca7-69decf20d810'} + deploy_info_mock.return_value = d_info + is_glance_image_mock.return_value = False + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + + _check_share_fs_mounted_mock.assert_called_once_with() + validate_mock.assert_called_once_with(task) + deploy_info_mock.assert_called_once_with(task.node) + validate_prop_mock.assert_called_once_with( + task.context, d_info, ['kernel', 'ramdisk']) + validate_capabilities_mock.assert_called_once_with(task.node) + + @mock.patch.object(irmc_deploy, '_reboot_into_deploy_iso', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_single_nic_with_vif_port_id', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'build_agent_options', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'check_image_size', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'cache_instance_image', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_deploy(self, + node_power_action_mock, + cache_instance_image_mock, + check_image_size_mock, + build_deploy_ramdisk_options_mock, + build_agent_options_mock, + get_single_nic_with_vif_port_id_mock, + _reboot_into_mock): + deploy_opts = {'a': 'b'} + build_agent_options_mock.return_value = { + 'ipa-api-url': 'http://1.2.3.4:6385'} + build_deploy_ramdisk_options_mock.return_value = deploy_opts + get_single_nic_with_vif_port_id_mock.return_value = '12:34:56:78:90:ab' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.deploy(task) + + node_power_action_mock.assert_called_once_with( + task, states.POWER_OFF) + cache_instance_image_mock.assert_called_once_with( + task.context, task.node) + check_image_size_mock.assert_called_once_with(task) + expected_ramdisk_opts = {'a': 'b', 'BOOTIF': '12:34:56:78:90:ab', + 'ipa-api-url': 'http://1.2.3.4:6385'} + build_agent_options_mock.assert_called_once_with(task.node) + build_deploy_ramdisk_options_mock.assert_called_once_with( + task.node) + get_single_nic_with_vif_port_id_mock.assert_called_once_with( + task) + _reboot_into_mock.assert_called_once_with( + task, expected_ramdisk_opts) + self.assertEqual(states.DEPLOYWAIT, returned_state) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_remove_share_file', spec_set=True, + autospec=True) + def test_tear_down(self, _remove_share_file_mock, node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['irmc_boot_iso'] = 'glance://deploy_iso' + task.node.driver_internal_info['irmc_boot_iso'] = 'irmc_boot.iso' + + returned_state = task.driver.deploy.tear_down(task) + + _remove_share_file_mock.assert_called_once_with( + irmc_deploy._get_boot_iso_name(task.node)) + node_power_action_mock.assert_called_once_with( + task, states.POWER_OFF) + self.assertFalse( + task.node.driver_internal_info.get('irmc_boot_iso')) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(iscsi_deploy, 'destroy_images', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_clean_up(self, _cleanup_vmedia_boot_mock, destroy_images_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.clean_up(task) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + destroy_images_mock.assert_called_once_with(task.node.uuid) + + +class IRMCVirtualMediaAgentDeployTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(IRMCVirtualMediaAgentDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="agent_irmc") + self.node = obj_utils.create_test_node( + self.context, driver='agent_irmc', driver_info=INFO_DICT) + + @mock.patch.object(deploy_utils, 'validate_capabilities', + spec_set=True, autospec=True) + @mock.patch.object(irmc_deploy, '_parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, _parse_driver_info_mock, + validate_capabilities_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + _parse_driver_info_mock.assert_called_once_with(task.node) + validate_capabilities_mock.assert_called_once_with(task.node) + + @mock.patch.object(irmc_deploy, '_reboot_into_deploy_iso', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'build_agent_options', spec_set=True, + autospec=True) + def test_deploy(self, build_agent_options_mock, + _reboot_into_deploy_iso_mock): + deploy_ramdisk_opts = build_agent_options_mock.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.deploy(task) + build_agent_options_mock.assert_called_once_with(task.node) + _reboot_into_deploy_iso_mock.assert_called_once_with( + task, deploy_ramdisk_opts) + self.assertEqual(states.DEPLOYWAIT, returned_state) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_tear_down(self, node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with( + task, states.POWER_OFF) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(agent, 'build_instance_info_for_deploy', spec_set=True, + autospec=True) + def test_prepare(self, build_instance_info_for_deploy_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.save = mock.MagicMock(sepc_set=[]) + task.driver.deploy.prepare(task) + build_instance_info_for_deploy_mock.assert_called_once_with( + task) + task.node.save.assert_called_once_with() + + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_clean_up(self, _cleanup_vmedia_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.clean_up(task) + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + + +class VendorPassthruTestCase(db_base.DbTestCase): + + def setUp(self): + irmc_deploy._check_share_fs_mounted_patcher.start() + super(VendorPassthruTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_irmc") + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_irmc', driver_info=INFO_DICT) + + CONF.irmc.remote_image_share_root = '/remote_image_share_root' + CONF.irmc.remote_image_server = '10.20.30.40' + CONF.irmc.remote_image_share_type = 'NFS' + CONF.irmc.remote_image_share_name = 'share' + CONF.irmc.remote_image_user_name = 'admin' + CONF.irmc.remote_image_user_password = 'admin0' + CONF.irmc.remote_image_user_domain = 'local' + + @mock.patch.object(iscsi_deploy, 'get_deploy_info', spec_set=True, + autospec=True) + def test_validate_pass_deploy_info(self, get_deploy_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.validate(task, method='pass_deploy_info', a=1) + get_deploy_info_mock.assert_called_once_with(task.node, a=1) + + @mock.patch.object(iscsi_deploy, 'validate_pass_bootloader_info_input', + spec_set=True, autospec=True) + def test_validate_pass_bootloader_install_info(self, validate_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + kwargs = {'address': '1.2.3.4', 'key': 'fake-key', + 'status': 'SUCCEEDED', 'error': ''} + task.driver.vendor.validate( + task, method='pass_bootloader_install_info', **kwargs) + validate_mock.assert_called_once_with(task, kwargs) + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_boot_iso', spec_set=True, + autospec=True) + def test__configure_vmedia_boot(self, + _prepare_boot_iso_mock, + setup_vmedia_for_boot_mock, + node_set_boot_device): + root_uuid_or_disk_id = {'root uuid': 'root_uuid'} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_internal_info['irmc_boot_iso'] = 'boot.iso' + task.driver.vendor._configure_vmedia_boot( + task, root_uuid_or_disk_id) + + _prepare_boot_iso_mock.assert_called_once_with( + task, root_uuid_or_disk_id) + setup_vmedia_for_boot_mock.assert_called_once_with( + task, 'boot.iso') + node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + + @mock.patch.object(iscsi_deploy, 'validate_bootloader_install_status', + spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'finish_deploy', spec_set=True, + autospec=True) + def test_pass_bootloader_install_info(self, finish_deploy_mock, + validate_input_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.pass_bootloader_install_info(task, **kwargs) + finish_deploy_mock.assert_called_once_with(task, '123456') + validate_input_mock.assert_called_once_with(task, kwargs) + + @mock.patch.object(deploy_utils, 'set_failed_state', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_boot_iso', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_ok(self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + _prepare_boot_iso_mock, + setup_vmedia_for_boot_mock, + node_set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + set_failed_state_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': 'root_uuid'} + + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_internal_info['irmc_boot_iso'] = 'irmc_boot.iso' + task.driver.vendor.pass_deploy_info(task, **kwargs) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + + _prepare_boot_iso_mock.assert_called_once_with( + task, 'root_uuid') + setup_vmedia_for_boot_mock.assert_called_once_with( + task, 'irmc_boot.iso') + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + notify_ramdisk_to_proceed_mock.assert_called_once_with( + '123456') + + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + self.assertFalse(set_failed_state_mock.called) + + @mock.patch.object(deploy_utils, 'set_failed_state', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_boot_iso', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_fail(self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + _prepare_boot_iso_mock, + setup_vmedia_for_boot_mock, + node_set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + set_failed_state_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + + self.node.provision_state = states.AVAILABLE + self.node.target_provision_state = states.NOSTATE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidState, + task.driver.vendor.pass_deploy_info, + task, **kwargs) + + self.assertEqual(states.AVAILABLE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + self.assertFalse(_cleanup_vmedia_boot_mock.called) + self.assertFalse(continue_deploy_mock.called) + self.assertFalse(_prepare_boot_iso_mock.called) + self.assertFalse(setup_vmedia_for_boot_mock.called) + self.assertFalse(node_set_boot_device_mock.called) + self.assertFalse(notify_ramdisk_to_proceed_mock.called) + self.assertFalse(set_failed_state_mock.called) + + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_prepare_boot_iso', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info__prepare_boot_exception( + self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + _prepare_boot_iso_mock, + setup_vmedia_for_boot_mock, + node_set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + node_power_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': 'root_uuid'} + _prepare_boot_iso_mock.side_effect = Exception("fake error") + + self.node.driver_internal_info = {'is_whole_disk_image': False} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.pass_deploy_info(task, **kwargs) + + continue_deploy_mock.assert_called_once_with( + task, method='pass_deploy_info', address='123456') + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + _prepare_boot_iso_mock.assert_called_once_with( + task, 'root_uuid') + + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertFalse(setup_vmedia_for_boot_mock.called) + self.assertFalse(node_set_boot_device_mock.called) + self.assertFalse(notify_ramdisk_to_proceed_mock.called) + node_power_mock.assert_called_once_with(task, states.POWER_OFF) + + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_localboot(self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + set_boot_device_mock, + notify_ramdisk_to_proceed_mock): + + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': '<some-uuid>'} + + self.node.driver_internal_info = {'is_whole_disk_image': False} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = task.driver.vendor + vendor.pass_deploy_info(task, **kwargs) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + notify_ramdisk_to_proceed_mock.assert_called_once_with('123456') + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + + @mock.patch.object(iscsi_deploy, 'finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_whole_disk_image( + self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + finish_deploy_mock): + + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': '<some-uuid>'} + + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = task.driver.vendor + vendor.pass_deploy_info(task, **kwargs) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + self.assertFalse(notify_ramdisk_to_proceed_mock.called) + finish_deploy_mock.assert_called_once_with(task, '123456') + + @mock.patch.object(iscsi_deploy, 'finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(iscsi_deploy, 'continue_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_pass_deploy_info_whole_disk_image_local( + self, + _cleanup_vmedia_boot_mock, + continue_deploy_mock, + set_boot_device_mock, + notify_ramdisk_to_proceed_mock, + finish_deploy_mock): + + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + continue_deploy_mock.return_value = {'root uuid': '<some-uuid>'} + + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vendor = task.driver.vendor + vendor.pass_deploy_info(task, **kwargs) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + continue_deploy_mock.assert_called_once_with(task, **kwargs) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + self.assertFalse(notify_ramdisk_to_proceed_mock.called) + finish_deploy_mock.assert_called_once_with(task, '123456') + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy.VendorPassthru, '_configure_vmedia_boot', + autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_continue_deploy_netboot(self, + _cleanup_vmedia_boot_mock, + do_agent_iscsi_deploy_mock, + _configure_vmedia_boot_mock, + reboot_and_finish_deploy_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'root uuid': 'some-root-uuid'} + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + _configure_vmedia_boot_mock.assert_called_once_with( + mock.ANY, task, 'some-root-uuid') + reboot_and_finish_deploy_mock.assert_called_once_with( + task.driver.vendor, task) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_continue_deploy_localboot(self, + _cleanup_vmedia_boot_mock, + do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + reboot_and_finish_deploy_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.instance_info = { + 'capabilities': {'boot_option': 'local'}} + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'root uuid': 'some-root-uuid'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + configure_local_boot_mock.assert_called_once_with( + mock.ANY, task, root_uuid='some-root-uuid', + efi_system_part_uuid=None) + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, task) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', spec_set=True, + autospec=True) + def test_continue_deploy_whole_disk_image(self, + _cleanup_vmedia_boot_mock, + do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + reboot_and_finish_deploy_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'disk identifier': 'some-disk-id'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + configure_local_boot_mock.assert_called_once_with( + mock.ANY, task, root_uuid=None, efi_system_part_uuid=None) + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, task) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', spec_set=True, + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', spec_set=True, autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', autospec=True) + def test_continue_deploy_localboot_uefi(self, + _cleanup_vmedia_boot_mock, + do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + reboot_and_finish_deploy_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.DEPLOYING + self.node.instance_info = { + 'capabilities': {'boot_option': 'local'}} + self.node.save() + do_agent_iscsi_deploy_mock.return_value = { + 'root uuid': 'some-root-uuid', + 'efi system partition uuid': 'efi-system-part-uuid'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.continue_deploy(task) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + do_agent_iscsi_deploy_mock.assert_called_once_with(task, + mock.ANY) + configure_local_boot_mock.assert_called_once_with( + mock.ANY, task, root_uuid='some-root-uuid', + efi_system_part_uuid='efi-system-part-uuid') + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, task) + + +class IRMCVirtualMediaAgentVendorInterfaceTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCVirtualMediaAgentVendorInterfaceTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="agent_irmc") + self.node = obj_utils.create_test_node( + self.context, driver='agent_irmc', driver_info=INFO_DICT) + + @mock.patch.object(agent.AgentVendorInterface, 'reboot_to_instance', + spec_set=True, autospec=True) + @mock.patch.object(irmc_deploy, '_cleanup_vmedia_boot', autospec=True) + def test_reboot_to_instance(self, + _cleanup_vmedia_boot_mock, + agent_reboot_to_instance_mock): + kwargs = {} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.reboot_to_instance(task, **kwargs) + + _cleanup_vmedia_boot_mock.assert_called_once_with(task) + agent_reboot_to_instance_mock.assert_called_once_with( + mock.ANY, task, **kwargs) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_management.py b/ironic/tests/unit/drivers/modules/irmc/test_management.py new file mode 100644 index 000000000..be75321ce --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_management.py @@ -0,0 +1,302 @@ +# Copyright 2015 FUJITSU LIMITED +# +# 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. + +""" +Test class for iRMC Management Driver +""" + +import os +import xml.etree.ElementTree as ET + +import mock + +from ironic.common import boot_devices +from ironic.common import driver_factory +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import management as irmc_management +from ironic.drivers import utils as driver_utils +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers import third_party_driver_mock_specs \ + as mock_specs +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = db_utils.get_test_irmc_info() + + +class IRMCManagementTestCase(db_base.DbTestCase): + def setUp(self): + super(IRMCManagementTestCase, self).setUp() + driver_info = INFO_DICT + + mgr_utils.mock_the_extension_manager(driver="fake_irmc") + self.driver = driver_factory.get_driver("fake_irmc") + self.node = obj_utils.create_test_node(self.context, + driver='fake_irmc', + driver_info=driver_info) + self.info = irmc_common.parse_driver_info(self.node) + + def test_get_properties(self): + expected = irmc_common.COMMON_PROPERTIES + expected.update(ipmitool.COMMON_PROPERTIES) + expected.update(ipmitool.CONSOLE_PROPERTIES) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(irmc_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.management.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(irmc_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate_fail(self, mock_drvinfo): + side_effect = iter([exception.InvalidParameterValue("Invalid Input")]) + mock_drvinfo.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.management.validate, + task) + + def test_management_interface_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + expected = [boot_devices.PXE, boot_devices.DISK, + boot_devices.CDROM, boot_devices.BIOS, + boot_devices.SAFE] + self.assertEqual(sorted(expected), sorted(task.driver.management. + get_supported_boot_devices(task))) + + @mock.patch.object(ipmitool.IPMIManagement, 'set_boot_device', + spec_set=True, autospec=True) + def test_management_interface_set_boot_device_no_mode_ok( + self, + set_boot_device_mock): + """no boot mode specified.""" + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, boot_devices.PXE) + set_boot_device_mock.assert_called_once_with( + task.driver.management, task, + boot_devices.PXE, + False) + + @mock.patch.object(ipmitool.IPMIManagement, 'set_boot_device', + spec_set=True, autospec=True) + def test_management_interface_set_boot_device_bios_ok( + self, + set_boot_device_mock): + """bios mode specified.""" + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_utils.add_node_capability(task, 'boot_mode', 'bios') + task.driver.management.set_boot_device(task, boot_devices.PXE) + set_boot_device_mock.assert_called_once_with( + task.driver.management, task, + boot_devices.PXE, + False) + + @mock.patch.object(irmc_management.ipmitool, "send_raw", spec_set=True, + autospec=True) + def _test_management_interface_set_boot_device_uefi_ok(self, params, + expected_raw_code, + send_raw_mock): + send_raw_mock.return_value = [None, None] + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.properties['capabilities'] = '' + driver_utils.add_node_capability(task, 'boot_mode', 'uefi') + self.driver.management.set_boot_device(task, **params) + send_raw_mock.assert_has_calls([ + mock.call(task, "0x00 0x08 0x03 0x08"), + mock.call(task, expected_raw_code)]) + + def test_management_interface_set_boot_device_uefi_ok_pxe(self): + params = {'device': boot_devices.PXE, 'persistent': False} + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xa0 0x04 0x00 0x00 0x00") + + params['persistent'] = True + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xe0 0x04 0x00 0x00 0x00") + + def test_management_interface_set_boot_device_uefi_ok_disk(self): + params = {'device': boot_devices.DISK, 'persistent': False} + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xa0 0x08 0x00 0x00 0x00") + + params['persistent'] = True + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xe0 0x08 0x00 0x00 0x00") + + def test_management_interface_set_boot_device_uefi_ok_cdrom(self): + params = {'device': boot_devices.CDROM, 'persistent': False} + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xa0 0x14 0x00 0x00 0x00") + + params['persistent'] = True + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xe0 0x14 0x00 0x00 0x00") + + def test_management_interface_set_boot_device_uefi_ok_bios(self): + params = {'device': boot_devices.BIOS, 'persistent': False} + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xa0 0x18 0x00 0x00 0x00") + + params['persistent'] = True + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xe0 0x18 0x00 0x00 0x00") + + def test_management_interface_set_boot_device_uefi_ok_safe(self): + params = {'device': boot_devices.SAFE, 'persistent': False} + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xa0 0x0c 0x00 0x00 0x00") + + params['persistent'] = True + self._test_management_interface_set_boot_device_uefi_ok( + params, + "0x00 0x08 0x05 0xe0 0x0c 0x00 0x00 0x00") + + @mock.patch.object(irmc_management.ipmitool, "send_raw", spec_set=True, + autospec=True) + def test_management_interface_set_boot_device_uefi_ng(self, + send_raw_mock): + """uefi mode, next boot only, unknown device.""" + send_raw_mock.return_value = [None, None] + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_utils.add_node_capability(task, 'boot_mode', 'uefi') + self.assertRaises(exception.InvalidParameterValue, + self.driver.management.set_boot_device, + task, + "unknown") + + @mock.patch.object(irmc_management, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + @mock.patch.object(irmc_common, 'get_irmc_report', spec_set=True, + autospec=True) + def test_management_interface_get_sensors_data_scci_ok( + self, mock_get_irmc_report, mock_scci): + """'irmc_sensor_method' = 'scci' specified and OK data.""" + with open(os.path.join(os.path.dirname(__file__), + 'fake_sensors_data_ok.xml'), "r") as report: + fake_txt = report.read() + fake_xml = ET.fromstring(fake_txt) + + mock_get_irmc_report.return_value = fake_xml + mock_scci.get_sensor_data.return_value = fake_xml.find( + "./System/SensorDataRecords") + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_sensor_method'] = 'scci' + sensor_dict = self.driver.management.get_sensors_data(task) + + expected = { + 'Fan (4)': { + 'FAN1 SYS (29)': { + 'Units': 'RPM', + 'Sensor ID': 'FAN1 SYS (29)', + 'Sensor Reading': '600 RPM' + }, + 'FAN2 SYS (29)': { + 'Units': 'None', + 'Sensor ID': 'FAN2 SYS (29)', + 'Sensor Reading': 'None None' + } + }, + 'Temperature (1)': { + 'Systemboard 1 (7)': { + 'Units': 'degree C', + 'Sensor ID': 'Systemboard 1 (7)', + 'Sensor Reading': '80 degree C' + }, + 'Ambient (55)': { + 'Units': 'degree C', + 'Sensor ID': 'Ambient (55)', + 'Sensor Reading': '42 degree C' + } + } + } + self.assertEqual(expected, sensor_dict) + + @mock.patch.object(irmc_management, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + @mock.patch.object(irmc_common, 'get_irmc_report', spec_set=True, + autospec=True) + def test_management_interface_get_sensors_data_scci_ng( + self, mock_get_irmc_report, mock_scci): + """'irmc_sensor_method' = 'scci' specified and NG data.""" + with open(os.path.join(os.path.dirname(__file__), + 'fake_sensors_data_ng.xml'), "r") as report: + fake_txt = report.read() + fake_xml = ET.fromstring(fake_txt) + + mock_get_irmc_report.return_value = fake_xml + mock_scci.get_sensor_data.return_value = fake_xml.find( + "./System/SensorDataRecords") + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_sensor_method'] = 'scci' + sensor_dict = self.driver.management.get_sensors_data(task) + + self.assertEqual(len(sensor_dict), 0) + + @mock.patch.object(ipmitool.IPMIManagement, 'get_sensors_data', + spec_set=True, autospec=True) + def test_management_interface_get_sensors_data_ipmitool_ok( + self, + get_sensors_data_mock): + """'irmc_sensor_method' = 'ipmitool' specified.""" + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_sensor_method'] = 'ipmitool' + task.driver.management.get_sensors_data(task) + get_sensors_data_mock.assert_called_once_with( + task.driver.management, task) + + @mock.patch.object(irmc_common, 'get_irmc_report', spec_set=True, + autospec=True) + def test_management_interface_get_sensors_data_exception( + self, + get_irmc_report_mock): + """'FailedToGetSensorData Exception.""" + + get_irmc_report_mock.side_effect = exception.InvalidParameterValue( + "Fake Error") + irmc_management.scci.SCCIInvalidInputError = Exception + irmc_management.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['irmc_sensor_method'] = 'scci' + e = self.assertRaises(exception.FailedToGetSensorData, + self.driver.management.get_sensors_data, + task) + self.assertEqual("Failed to get sensor data for node 1be26c0b-" + + "03f2-4d2e-ae87-c02d7f33c123. Error: Fake Error", + str(e)) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_power.py b/ironic/tests/unit/drivers/modules/irmc/test_power.py new file mode 100644 index 000000000..1b51f9eac --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_power.py @@ -0,0 +1,224 @@ +# Copyright 2015 FUJITSU LIMITED +# +# 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. + +""" +Test class for iRMC Power Driver +""" + +import mock +from oslo_config import cfg + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules.irmc import common as irmc_common +from ironic.drivers.modules.irmc import deploy as irmc_deploy +from ironic.drivers.modules.irmc import power as irmc_power +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INFO_DICT = db_utils.get_test_irmc_info() +CONF = cfg.CONF + + +@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True, + autospec=True) +class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IRMCPowerInternalMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_irmc') + driver_info = INFO_DICT + self.node = db_utils.create_test_node( + driver='fake_irmc', + driver_info=driver_info, + instance_uuid='instance_uuid_123') + + @mock.patch.object(irmc_power, '_attach_boot_iso_if_needed') + def test__set_power_state_power_on_ok( + self, + _attach_boot_iso_if_needed_mock, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + _attach_boot_iso_if_needed_mock.assert_called_once_with(task) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_ON) + + def test__set_power_state_power_off_ok(self, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.POWER_OFF + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_OFF) + + @mock.patch.object(irmc_power, '_attach_boot_iso_if_needed') + def test__set_power_state_power_reboot_ok( + self, + _attach_boot_iso_if_needed_mock, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + target_state = states.REBOOT + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + irmc_power._set_power_state(task, target_state) + _attach_boot_iso_if_needed_mock.assert_called_once_with(task) + irmc_client.assert_called_once_with(irmc_power.scci.POWER_RESET) + + def test__set_power_state_invalid_target_state(self, + get_irmc_client_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + irmc_power._set_power_state, + task, + states.ERROR) + + def test__set_power_state_scci_exception(self, + get_irmc_client_mock): + irmc_client = get_irmc_client_mock.return_value + irmc_client.side_effect = Exception() + irmc_power.scci.SCCIClientError = Exception + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_power._set_power_state, + task, + states.POWER_ON) + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + def test__attach_boot_iso_if_needed( + self, + setup_vmedia_mock, + set_boot_device_mock, + get_irmc_client_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.ACTIVE + task.node.driver_internal_info['irmc_boot_iso'] = 'boot-iso' + irmc_power._attach_boot_iso_if_needed(task) + setup_vmedia_mock.assert_called_once_with(task, 'boot-iso') + set_boot_device_mock.assert_called_once_with( + task, boot_devices.CDROM) + + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(irmc_deploy, 'setup_vmedia_for_boot', spec_set=True, + autospec=True) + def test__attach_boot_iso_if_needed_on_rebuild( + self, + setup_vmedia_mock, + set_boot_device_mock, + get_irmc_client_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info['irmc_boot_iso'] = 'boot-iso' + irmc_power._attach_boot_iso_if_needed(task) + self.assertFalse(setup_vmedia_mock.called) + self.assertFalse(set_boot_device_mock.called) + + +class IRMCPowerTestCase(db_base.DbTestCase): + def setUp(self): + super(IRMCPowerTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver="fake_irmc") + self.node = obj_utils.create_test_node(self.context, + driver='fake_irmc', + driver_info=driver_info) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + properties = task.driver.get_properties() + for prop in irmc_common.COMMON_PROPERTIES: + self.assertIn(prop, properties) + + @mock.patch.object(irmc_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(irmc_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate_fail(self, mock_drvinfo): + side_effect = iter([exception.InvalidParameterValue("Invalid Input")]) + mock_drvinfo.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, + task) + + @mock.patch('ironic.drivers.modules.irmc.power.ipmitool.IPMIPower', + spec_set=True, autospec=True) + def test_get_power_state(self, mock_IPMIPower): + ipmi_power = mock_IPMIPower.return_value + ipmi_power.get_power_state.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(states.POWER_ON, + task.driver.power.get_power_state(task)) + ipmi_power.get_power_state.assert_called_once_with(task) + + @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, + autospec=True) + def test_set_power_state(self, mock_set_power): + mock_set_power.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + mock_set_power.assert_called_once_with(task, states.POWER_ON) + + @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_power.IRMCPower, 'get_power_state', spec_set=True, + autospec=True) + def test_reboot_reboot(self, mock_get_power, mock_set_power): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_get_power.return_value = states.POWER_ON + task.driver.power.reboot(task) + mock_get_power.assert_called_once_with( + task.driver.power, task) + mock_set_power.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(irmc_power, '_set_power_state', spec_set=True, + autospec=True) + @mock.patch.object(irmc_power.IRMCPower, 'get_power_state', spec_set=True, + autospec=True) + def test_reboot_power_on(self, mock_get_power, mock_set_power): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_get_power.return_value = states.POWER_OFF + task.driver.power.reboot(task) + mock_get_power.assert_called_once_with( + task.driver.power, task) + mock_set_power.assert_called_once_with(task, states.POWER_ON) diff --git a/ironic/tests/unit/drivers/modules/msftocs/__init__.py b/ironic/tests/unit/drivers/modules/msftocs/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/msftocs/__init__.py diff --git a/ironic/tests/unit/drivers/modules/msftocs/test_common.py b/ironic/tests/unit/drivers/modules/msftocs/test_common.py new file mode 100644 index 000000000..c5891a743 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/msftocs/test_common.py @@ -0,0 +1,110 @@ +# Copyright 2015 Cloudbase Solutions Srl +# 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. + +""" +Test class for MSFT OCS common functions +""" + +import mock + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.msftocs import common as msftocs_common +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INFO_DICT = db_utils.get_test_msftocs_info() + + +class MSFTOCSCommonTestCase(db_base.DbTestCase): + def setUp(self): + super(MSFTOCSCommonTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_msftocs') + self.info = INFO_DICT + self.node = obj_utils.create_test_node(self.context, + driver='fake_msftocs', + driver_info=self.info) + + def test_get_client_info(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + driver_info = task.node.driver_info + (client, blade_id) = msftocs_common.get_client_info(driver_info) + + self.assertEqual(driver_info['msftocs_base_url'], client._base_url) + self.assertEqual(driver_info['msftocs_username'], client._username) + self.assertEqual(driver_info['msftocs_password'], client._password) + self.assertEqual(driver_info['msftocs_blade_id'], blade_id) + + @mock.patch.object(msftocs_common, '_is_valid_url', autospec=True) + def test_parse_driver_info(self, mock_is_valid_url): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + msftocs_common.parse_driver_info(task.node) + mock_is_valid_url.assert_called_once_with( + task.node.driver_info['msftocs_base_url']) + + def test_parse_driver_info_fail_missing_param(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + del task.node.driver_info['msftocs_base_url'] + self.assertRaises(exception.MissingParameterValue, + msftocs_common.parse_driver_info, + task.node) + + def test_parse_driver_info_fail_bad_url(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info['msftocs_base_url'] = "bad-url" + self.assertRaises(exception.InvalidParameterValue, + msftocs_common.parse_driver_info, + task.node) + + def test_parse_driver_info_fail_bad_blade_id_type(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info['msftocs_blade_id'] = "bad-blade-id" + self.assertRaises(exception.InvalidParameterValue, + msftocs_common.parse_driver_info, + task.node) + + def test_parse_driver_info_fail_bad_blade_id_value(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info['msftocs_blade_id'] = 0 + self.assertRaises(exception.InvalidParameterValue, + msftocs_common.parse_driver_info, + task.node) + + def test__is_valid_url(self): + self.assertIs(True, msftocs_common._is_valid_url("http://fake.com")) + self.assertIs( + True, msftocs_common._is_valid_url("http://www.fake.com")) + self.assertIs(True, msftocs_common._is_valid_url("http://FAKE.com")) + self.assertIs(True, msftocs_common._is_valid_url("http://fake")) + self.assertIs( + True, msftocs_common._is_valid_url("http://fake.com/blah")) + self.assertIs(True, msftocs_common._is_valid_url("http://localhost")) + self.assertIs(True, msftocs_common._is_valid_url("https://fake.com")) + self.assertIs(True, msftocs_common._is_valid_url("http://10.0.0.1")) + self.assertIs(False, msftocs_common._is_valid_url("bad-url")) + self.assertIs(False, msftocs_common._is_valid_url("http://.bad-url")) + self.assertIs(False, msftocs_common._is_valid_url("http://bad-url$")) + self.assertIs(False, msftocs_common._is_valid_url("http://$bad-url")) + self.assertIs(False, msftocs_common._is_valid_url("http://bad$url")) + self.assertIs(False, msftocs_common._is_valid_url(None)) + self.assertIs(False, msftocs_common._is_valid_url(0)) diff --git a/ironic/tests/unit/drivers/modules/msftocs/test_management.py b/ironic/tests/unit/drivers/modules/msftocs/test_management.py new file mode 100644 index 000000000..a121a778d --- /dev/null +++ b/ironic/tests/unit/drivers/modules/msftocs/test_management.py @@ -0,0 +1,133 @@ +# Copyright 2015 Cloudbase Solutions Srl +# 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. + +""" +Test class for MSFT OCS ManagementInterface +""" + +import mock + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.msftocs import common as msftocs_common +from ironic.drivers.modules.msftocs import msftocsclient +from ironic.drivers import utils as drivers_utils +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INFO_DICT = db_utils.get_test_msftocs_info() + + +class MSFTOCSManagementTestCase(db_base.DbTestCase): + def setUp(self): + super(MSFTOCSManagementTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_msftocs') + self.info = INFO_DICT + self.node = obj_utils.create_test_node(self.context, + driver='fake_msftocs', + driver_info=self.info) + + def test_get_properties(self): + expected = msftocs_common.REQUIRED_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True) + def test_validate_fail(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_drvinfo.side_effect = iter( + [exception.InvalidParameterValue('x')]) + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, + task) + + def test_get_supported_boot_devices(self): + expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.BIOS] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual( + sorted(expected), + sorted(task.driver.management. + get_supported_boot_devices(task))) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def _test_set_boot_device_one_time(self, persistent, uefi, + mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + + if uefi: + drivers_utils.add_node_capability(task, 'boot_mode', 'uefi') + + task.driver.management.set_boot_device( + task, boot_devices.PXE, persistent) + + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.set_next_boot.assert_called_once_with( + blade_id, msftocsclient.BOOT_TYPE_FORCE_PXE, persistent, uefi) + + def test_set_boot_device_one_time(self): + self._test_set_boot_device_one_time(False, False) + + def test_set_boot_device_persistent(self): + self._test_set_boot_device_one_time(True, False) + + def test_set_boot_device_uefi(self): + self._test_set_boot_device_one_time(True, True) + + def test_set_boot_device_fail(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.management.set_boot_device, + task, 'fake-device') + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_get_boot_device(self, mock_gci): + expected = {'boot_device': boot_devices.DISK, 'persistent': None} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + force_hdd = msftocsclient.BOOT_TYPE_FORCE_DEFAULT_HDD + mock_c.get_next_boot.return_value = force_hdd + + self.assertEqual(expected, + task.driver.management.get_boot_device(task)) + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.get_next_boot.assert_called_once_with(blade_id) + + def test_get_sensor_data(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(NotImplementedError, + task.driver.management.get_sensors_data, + task) diff --git a/ironic/tests/unit/drivers/modules/msftocs/test_msftocsclient.py b/ironic/tests/unit/drivers/modules/msftocs/test_msftocsclient.py new file mode 100644 index 000000000..15be486c5 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/msftocs/test_msftocsclient.py @@ -0,0 +1,182 @@ +# Copyright 2015 Cloudbase Solutions Srl +# 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. + +""" +Test class for MSFT OCS REST API client +""" + +import mock +import requests +from requests import exceptions as requests_exceptions + +from ironic.common import exception +from ironic.drivers.modules.msftocs import msftocsclient +from ironic.tests.unit import base + + +FAKE_BOOT_RESPONSE = ( + '<BootResponse xmlns="%s" ' + 'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' + '<completionCode>Success</completionCode>' + '<apiVersion>1</apiVersion>' + '<statusDescription>Success</statusDescription>' + '<bladeNumber>1</bladeNumber>' + '<nextBoot>ForcePxe</nextBoot>' + '</BootResponse>') % msftocsclient.WCSNS + +FAKE_BLADE_RESPONSE = ( + '<BladeResponse xmlns="%s" ' + 'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' + '<completionCode>Success</completionCode>' + '<apiVersion>1</apiVersion>' + '<statusDescription/>' + '<bladeNumber>1</bladeNumber>' + '</BladeResponse>') % msftocsclient.WCSNS + +FAKE_POWER_STATE_RESPONSE = ( + '<PowerStateResponse xmlns="%s" ' + 'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' + '<completionCode>Success</completionCode>' + '<apiVersion>1</apiVersion>' + '<statusDescription>Blade Power is On, firmware decompressed' + '</statusDescription>' + '<bladeNumber>1</bladeNumber>' + '<Decompression>0</Decompression>' + '<powerState>ON</powerState>' + '</PowerStateResponse>') % msftocsclient.WCSNS + +FAKE_BLADE_STATE_RESPONSE = ( + '<BladeStateResponse xmlns="%s" ' + 'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' + '<completionCode>Success</completionCode>' + '<apiVersion>1</apiVersion>' + '<statusDescription/>' + '<bladeNumber>1</bladeNumber>' + '<bladeState>ON</bladeState>' + '</BladeStateResponse>') % msftocsclient.WCSNS + + +class MSFTOCSClientApiTestCase(base.TestCase): + def setUp(self): + super(MSFTOCSClientApiTestCase, self).setUp() + self._fake_base_url = "http://fakehost:8000" + self._fake_username = "admin" + self._fake_password = 'fake' + self._fake_blade_id = 1 + self._client = msftocsclient.MSFTOCSClientApi( + self._fake_base_url, self._fake_username, self._fake_password) + + @mock.patch.object(requests, 'get', autospec=True) + def test__exec_cmd(self, mock_get): + fake_response_text = 'fake_response_text' + fake_rel_url = 'fake_rel_url' + mock_get.return_value.text = 'fake_response_text' + + self.assertEqual(fake_response_text, + self._client._exec_cmd(fake_rel_url)) + mock_get.assert_called_once_with( + self._fake_base_url + "/" + fake_rel_url, auth=mock.ANY) + + @mock.patch.object(requests, 'get', autospec=True) + def test__exec_cmd_http_get_fail(self, mock_get): + fake_rel_url = 'fake_rel_url' + mock_get.side_effect = iter([requests_exceptions.ConnectionError('x')]) + + self.assertRaises(exception.MSFTOCSClientApiException, + self._client._exec_cmd, + fake_rel_url) + mock_get.assert_called_once_with( + self._fake_base_url + "/" + fake_rel_url, auth=mock.ANY) + + def test__check_completion_code(self): + et = self._client._check_completion_code(FAKE_BOOT_RESPONSE) + self.assertEqual('{%s}BootResponse' % msftocsclient.WCSNS, et.tag) + + def test__check_completion_code_fail(self): + self.assertRaises(exception.MSFTOCSClientApiException, + self._client._check_completion_code, + '<fake xmlns="%s"></fake>' % msftocsclient.WCSNS) + + def test__check_completion_with_bad_completion_code_fail(self): + self.assertRaises(exception.MSFTOCSClientApiException, + self._client._check_completion_code, + '<fake xmlns="%s">' + '<completionCode>Fail</completionCode>' + '</fake>' % msftocsclient.WCSNS) + + def test__check_completion_code_xml_parsing_fail(self): + self.assertRaises(exception.MSFTOCSClientApiException, + self._client._check_completion_code, + 'bad_xml') + + @mock.patch.object( + msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True) + def test_get_blade_state(self, mock_exec_cmd): + mock_exec_cmd.return_value = FAKE_BLADE_STATE_RESPONSE + self.assertEqual( + msftocsclient.POWER_STATUS_ON, + self._client.get_blade_state(self._fake_blade_id)) + mock_exec_cmd.assert_called_once_with( + self._client, "GetBladeState?bladeId=%d" % self._fake_blade_id) + + @mock.patch.object( + msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True) + def test_set_blade_on(self, mock_exec_cmd): + mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE + self._client.set_blade_on(self._fake_blade_id) + mock_exec_cmd.assert_called_once_with( + self._client, "SetBladeOn?bladeId=%d" % self._fake_blade_id) + + @mock.patch.object( + msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True) + def test_set_blade_off(self, mock_exec_cmd): + mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE + self._client.set_blade_off(self._fake_blade_id) + mock_exec_cmd.assert_called_once_with( + self._client, "SetBladeOff?bladeId=%d" % self._fake_blade_id) + + @mock.patch.object( + msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True) + def test_set_blade_power_cycle(self, mock_exec_cmd): + mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE + self._client.set_blade_power_cycle(self._fake_blade_id) + mock_exec_cmd.assert_called_once_with( + self._client, + "SetBladeActivePowerCycle?bladeId=%d&offTime=0" % + self._fake_blade_id) + + @mock.patch.object( + msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True) + def test_get_next_boot(self, mock_exec_cmd): + mock_exec_cmd.return_value = FAKE_BOOT_RESPONSE + self.assertEqual( + msftocsclient.BOOT_TYPE_FORCE_PXE, + self._client.get_next_boot(self._fake_blade_id)) + mock_exec_cmd.assert_called_once_with( + self._client, "GetNextBoot?bladeId=%d" % self._fake_blade_id) + + @mock.patch.object( + msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True) + def test_set_next_boot(self, mock_exec_cmd): + mock_exec_cmd.return_value = FAKE_BOOT_RESPONSE + self._client.set_next_boot(self._fake_blade_id, + msftocsclient.BOOT_TYPE_FORCE_PXE) + mock_exec_cmd.assert_called_once_with( + self._client, + "SetNextBoot?bladeId=%(blade_id)d&bootType=%(boot_type)d&" + "uefi=%(uefi)s&persistent=%(persistent)s" % + {"blade_id": self._fake_blade_id, + "boot_type": msftocsclient.BOOT_TYPE_FORCE_PXE, + "uefi": "true", "persistent": "true"}) diff --git a/ironic/tests/unit/drivers/modules/msftocs/test_power.py b/ironic/tests/unit/drivers/modules/msftocs/test_power.py new file mode 100644 index 000000000..aeeabe710 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/msftocs/test_power.py @@ -0,0 +1,164 @@ +# Copyright 2015 Cloudbase Solutions Srl +# 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. + +""" +Test class for MSFT OCS PowerInterface +""" + +import mock + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.msftocs import common as msftocs_common +from ironic.drivers.modules.msftocs import msftocsclient +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INFO_DICT = db_utils.get_test_msftocs_info() + + +class MSFTOCSPowerTestCase(db_base.DbTestCase): + def setUp(self): + super(MSFTOCSPowerTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_msftocs') + self.info = INFO_DICT + self.node = obj_utils.create_test_node(self.context, + driver='fake_msftocs', + driver_info=self.info) + + def test_get_properties(self): + expected = msftocs_common.REQUIRED_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True) + def test_validate_fail(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_drvinfo.side_effect = iter( + [exception.InvalidParameterValue('x')]) + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, + task) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_get_power_state(self, mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + mock_c.get_blade_state.return_value = msftocsclient.POWER_STATUS_ON + + self.assertEqual(states.POWER_ON, + task.driver.power.get_power_state(task)) + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.get_blade_state.assert_called_once_with(blade_id) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_set_power_state_on(self, mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + + task.driver.power.set_power_state(task, states.POWER_ON) + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.set_blade_on.assert_called_once_with(blade_id) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_set_power_state_off(self, mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + + task.driver.power.set_power_state(task, states.POWER_OFF) + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.set_blade_off.assert_called_once_with(blade_id) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_set_power_state_blade_on_fail(self, mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + + ex = exception.MSFTOCSClientApiException('x') + mock_c.set_blade_on.side_effect = ex + + pstate = states.POWER_ON + self.assertRaises(exception.PowerStateFailure, + task.driver.power.set_power_state, + task, pstate) + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.set_blade_on.assert_called_once_with(blade_id) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_set_power_state_invalid_parameter_fail(self, mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + + pstate = states.ERROR + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, pstate) + mock_gci.assert_called_once_with(task.node.driver_info) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_reboot(self, mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + + task.driver.power.reboot(task) + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.set_blade_power_cycle.assert_called_once_with(blade_id) + + @mock.patch.object(msftocs_common, 'get_client_info', autospec=True) + def test_reboot_fail(self, mock_gci): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi) + blade_id = task.node.driver_info['msftocs_blade_id'] + mock_gci.return_value = (mock_c, blade_id) + + ex = exception.MSFTOCSClientApiException('x') + mock_c.set_blade_power_cycle.side_effect = ex + + self.assertRaises(exception.PowerStateFailure, + task.driver.power.reboot, + task) + mock_gci.assert_called_once_with(task.node.driver_info) + mock_c.set_blade_power_cycle.assert_called_once_with(blade_id) diff --git a/ironic/tests/unit/drivers/modules/test_agent.py b/ironic/tests/unit/drivers/modules/test_agent.py new file mode 100644 index 000000000..41fe87a77 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_agent.py @@ -0,0 +1,805 @@ +# Copyright 2014 Rackspace, Inc. +# +# 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 types + +import mock +from oslo_config import cfg + +from ironic.common import exception +from ironic.common import image_service +from ironic.common import images +from ironic.common import raid +from ironic.common import states +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import agent +from ironic.drivers.modules import agent_base_vendor +from ironic.drivers.modules import agent_client +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules import fake +from ironic.drivers.modules import pxe +from ironic.tests.unit.conductor import utils as mgr_utils +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 object_utils + + +INSTANCE_INFO = db_utils.get_test_agent_instance_info() +DRIVER_INFO = db_utils.get_test_agent_driver_info() +DRIVER_INTERNAL_INFO = db_utils.get_test_agent_driver_internal_info() + +CONF = cfg.CONF + + +class TestAgentMethods(db_base.DbTestCase): + def setUp(self): + super(TestAgentMethods, self).setUp() + self.node = object_utils.create_test_node(self.context, + driver='fake_agent') + + @mock.patch.object(image_service, 'GlanceImageService', autospec=True) + def test_build_instance_info_for_deploy_glance_image(self, glance_mock): + i_info = self.node.instance_info + i_info['image_source'] = '733d1c44-a2ea-414b-aca7-69decf20d810' + self.node.instance_info = i_info + self.node.save() + + image_info = {'checksum': 'aa', 'disk_format': 'qcow2', + 'container_format': 'bare'} + glance_mock.return_value.show = mock.MagicMock(spec_set=[], + return_value=image_info) + + mgr_utils.mock_the_extension_manager(driver='fake_agent') + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + + agent.build_instance_info_for_deploy(task) + + glance_mock.assert_called_once_with(version=2, + context=task.context) + glance_mock.return_value.show.assert_called_once_with( + self.node.instance_info['image_source']) + glance_mock.return_value.swift_temp_url.assert_called_once_with( + image_info) + + @mock.patch.object(image_service.HttpImageService, 'validate_href', + autospec=True) + def test_build_instance_info_for_deploy_nonglance_image( + self, validate_href_mock): + i_info = self.node.instance_info + i_info['image_source'] = 'http://image-ref' + i_info['image_checksum'] = 'aa' + self.node.instance_info = i_info + self.node.save() + + mgr_utils.mock_the_extension_manager(driver='fake_agent') + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + + info = agent.build_instance_info_for_deploy(task) + + self.assertEqual(self.node.instance_info['image_source'], + info['image_url']) + validate_href_mock.assert_called_once_with( + mock.ANY, 'http://image-ref') + + @mock.patch.object(image_service.HttpImageService, 'validate_href', + autospec=True) + def test_build_instance_info_for_deploy_nonsupported_image( + self, validate_href_mock): + validate_href_mock.side_effect = iter( + [exception.ImageRefValidationFailed( + image_href='file://img.qcow2', reason='fail')]) + i_info = self.node.instance_info + i_info['image_source'] = 'file://img.qcow2' + i_info['image_checksum'] = 'aa' + self.node.instance_info = i_info + self.node.save() + + mgr_utils.mock_the_extension_manager(driver='fake_agent') + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + + self.assertRaises(exception.ImageRefValidationFailed, + agent.build_instance_info_for_deploy, task) + + @mock.patch.object(images, 'download_size', autospec=True) + def test_check_image_size(self, size_mock): + size_mock.return_value = 10 * 1024 * 1024 + mgr_utils.mock_the_extension_manager(driver='fake_agent') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties['memory_mb'] = 10 + agent.check_image_size(task, 'fake-image') + size_mock.assert_called_once_with(self.context, 'fake-image') + + @mock.patch.object(images, 'download_size', autospec=True) + def test_check_image_size_without_memory_mb(self, size_mock): + mgr_utils.mock_the_extension_manager(driver='fake_agent') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties.pop('memory_mb', None) + agent.check_image_size(task, 'fake-image') + self.assertFalse(size_mock.called) + + @mock.patch.object(images, 'download_size', autospec=True) + def test_check_image_size_fail(self, size_mock): + size_mock.return_value = 11 * 1024 * 1024 + + mgr_utils.mock_the_extension_manager(driver='fake_agent') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties['memory_mb'] = 10 + self.assertRaises(exception.InvalidParameterValue, + agent.check_image_size, + task, 'fake-image') + size_mock.assert_called_once_with(self.context, 'fake-image') + + @mock.patch.object(images, 'download_size', autospec=True) + def test_check_image_size_fail_by_agent_consumed_memory(self, size_mock): + self.config(memory_consumed_by_agent=2, group='agent') + size_mock.return_value = 9 * 1024 * 1024 + mgr_utils.mock_the_extension_manager(driver='fake_agent') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.properties['memory_mb'] = 10 + self.assertRaises(exception.InvalidParameterValue, + agent.check_image_size, + task, 'fake-image') + size_mock.assert_called_once_with(self.context, 'fake-image') + + +class TestAgentDeploy(db_base.DbTestCase): + def setUp(self): + super(TestAgentDeploy, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_agent') + self.driver = agent.AgentDeploy() + n = { + 'driver': 'fake_agent', + 'instance_info': INSTANCE_INFO, + 'driver_info': DRIVER_INFO, + 'driver_internal_info': DRIVER_INTERNAL_INFO, + } + self.node = object_utils.create_test_node(self.context, **n) + self.ports = [ + object_utils.create_test_port(self.context, node_id=self.node.id)] + + def test_get_properties(self): + expected = agent.COMMON_PROPERTIES + self.assertEqual(expected, self.driver.get_properties()) + + @mock.patch.object(images, 'download_size', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate(self, pxe_boot_validate_mock, size_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + self.driver.validate(task) + pxe_boot_validate_mock.assert_called_once_with( + task.driver.boot, task) + size_mock.assert_called_once_with(self.context, 'fake-image') + + @mock.patch.object(images, 'download_size', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate_driver_info_manage_agent_boot_false( + self, pxe_boot_validate_mock, size_mock): + self.config(manage_agent_boot=False, group='agent') + self.node.driver_info = {} + self.node.save() + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + self.driver.validate(task) + self.assertFalse(pxe_boot_validate_mock.called) + size_mock.assert_called_once_with(self.context, 'fake-image') + + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate_instance_info_missing_params( + self, pxe_boot_validate_mock): + self.node.instance_info = {} + self.node.save() + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + e = self.assertRaises(exception.MissingParameterValue, + self.driver.validate, task) + pxe_boot_validate_mock.assert_called_once_with( + task.driver.boot, task) + + self.assertIn('instance_info.image_source', str(e)) + + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate_nonglance_image_no_checksum( + self, pxe_boot_validate_mock): + i_info = self.node.instance_info + i_info['image_source'] = 'http://image-ref' + del i_info['image_checksum'] + self.node.instance_info = i_info + self.node.save() + + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + self.assertRaises(exception.MissingParameterValue, + self.driver.validate, task) + pxe_boot_validate_mock.assert_called_once_with( + task.driver.boot, task) + + @mock.patch.object(images, 'download_size', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate_agent_fail_partition_image( + self, pxe_boot_validate_mock, size_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = False + self.assertRaises(exception.InvalidParameterValue, + self.driver.validate, task) + pxe_boot_validate_mock.assert_called_once_with( + task.driver.boot, task) + size_mock.assert_called_once_with(self.context, 'fake-image') + + @mock.patch.object(images, 'download_size', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate_invalid_root_device_hints( + self, pxe_boot_validate_mock, size_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties['root_device'] = {'size': 'not-int'} + self.assertRaises(exception.InvalidParameterValue, + task.driver.deploy.validate, task) + pxe_boot_validate_mock.assert_called_once_with( + task.driver.boot, task) + size_mock.assert_called_once_with(self.context, 'fake-image') + + @mock.patch('ironic.conductor.utils.node_power_action', autospec=True) + def test_deploy(self, power_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + driver_return = self.driver.deploy(task) + self.assertEqual(driver_return, states.DEPLOYWAIT) + power_mock.assert_called_once_with(task, states.REBOOT) + + @mock.patch('ironic.conductor.utils.node_power_action', autospec=True) + def test_tear_down(self, power_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + driver_return = self.driver.tear_down(task) + power_mock.assert_called_once_with(task, states.POWER_OFF) + self.assertEqual(driver_return, states.DELETED) + + @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk') + @mock.patch.object(deploy_utils, 'build_agent_options') + @mock.patch.object(agent, 'build_instance_info_for_deploy') + def test_prepare(self, build_instance_info_mock, build_options_mock, + pxe_prepare_ramdisk_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + task.node.provision_state = states.DEPLOYING + build_instance_info_mock.return_value = {'foo': 'bar'} + build_options_mock.return_value = {'a': 'b'} + + self.driver.prepare(task) + + build_instance_info_mock.assert_called_once_with(task) + build_options_mock.assert_called_once_with(task.node) + pxe_prepare_ramdisk_mock.assert_called_once_with( + task, {'a': 'b'}) + + self.node.refresh() + self.assertEqual('bar', self.node.instance_info['foo']) + + @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk') + @mock.patch.object(deploy_utils, 'build_agent_options') + @mock.patch.object(agent, 'build_instance_info_for_deploy') + def test_prepare_manage_agent_boot_false( + self, build_instance_info_mock, build_options_mock, + pxe_prepare_ramdisk_mock): + self.config(group='agent', manage_agent_boot=False) + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + task.node.provision_state = states.DEPLOYING + build_instance_info_mock.return_value = {'foo': 'bar'} + + self.driver.prepare(task) + + build_instance_info_mock.assert_called_once_with(task) + self.assertFalse(build_options_mock.called) + self.assertFalse(pxe_prepare_ramdisk_mock.called) + + self.node.refresh() + self.assertEqual('bar', self.node.instance_info['foo']) + + @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk') + @mock.patch.object(deploy_utils, 'build_agent_options') + @mock.patch.object(agent, 'build_instance_info_for_deploy') + def test_prepare_active( + self, build_instance_info_mock, build_options_mock, + pxe_prepare_ramdisk_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + task.node.provision_state = states.ACTIVE + + self.driver.prepare(task) + + self.assertFalse(build_instance_info_mock.called) + self.assertFalse(build_options_mock.called) + self.assertFalse(pxe_prepare_ramdisk_mock.called) + + @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk') + def test_clean_up(self, pxe_clean_up_ramdisk_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + self.driver.clean_up(task) + pxe_clean_up_ramdisk_mock.assert_called_once_with(task) + + @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk') + def test_clean_up_manage_agent_boot_false(self, pxe_clean_up_ramdisk_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + self.config(group='agent', manage_agent_boot=False) + self.driver.clean_up(task) + self.assertFalse(pxe_clean_up_ramdisk_mock.called) + + @mock.patch('ironic.drivers.modules.deploy_utils.agent_get_clean_steps', + autospec=True) + def test_get_clean_steps(self, mock_get_clean_steps): + # Test getting clean steps + mock_steps = [{'priority': 10, 'interface': 'deploy', + 'step': 'erase_devices'}] + mock_get_clean_steps.return_value = mock_steps + with task_manager.acquire(self.context, self.node.uuid) as task: + steps = self.driver.get_clean_steps(task) + mock_get_clean_steps.assert_called_once_with(task) + self.assertEqual(mock_steps, steps) + + @mock.patch('ironic.drivers.modules.deploy_utils.agent_get_clean_steps', + autospec=True) + def test_get_clean_steps_config_priority(self, mock_get_clean_steps): + # Test that we can override the priority of get clean steps + # Use 0 because it is an edge case (false-y) and used in devstack + self.config(erase_devices_priority=0, group='deploy') + mock_steps = [{'priority': 10, 'interface': 'deploy', + 'step': 'erase_devices'}] + expected_steps = [{'priority': 0, 'interface': 'deploy', + 'step': 'erase_devices'}] + mock_get_clean_steps.return_value = mock_steps + with task_manager.acquire(self.context, self.node.uuid) as task: + steps = self.driver.get_clean_steps(task) + mock_get_clean_steps.assert_called_once_with(task) + self.assertEqual(expected_steps, steps) + + @mock.patch.object(deploy_utils, 'prepare_inband_cleaning', autospec=True) + def test_prepare_cleaning(self, prepare_inband_cleaning_mock): + prepare_inband_cleaning_mock.return_value = states.CLEANWAIT + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual( + states.CLEANWAIT, self.driver.prepare_cleaning(task)) + prepare_inband_cleaning_mock.assert_called_once_with( + task, manage_boot=True) + + @mock.patch.object(deploy_utils, 'prepare_inband_cleaning', autospec=True) + def test_prepare_cleaning_manage_agent_boot_false( + self, prepare_inband_cleaning_mock): + prepare_inband_cleaning_mock.return_value = states.CLEANWAIT + self.config(group='agent', manage_agent_boot=False) + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual( + states.CLEANWAIT, self.driver.prepare_cleaning(task)) + prepare_inband_cleaning_mock.assert_called_once_with( + task, manage_boot=False) + + @mock.patch.object(deploy_utils, 'tear_down_inband_cleaning', + autospec=True) + def test_tear_down_cleaning(self, tear_down_cleaning_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.driver.tear_down_cleaning(task) + tear_down_cleaning_mock.assert_called_once_with( + task, manage_boot=True) + + @mock.patch.object(deploy_utils, 'tear_down_inband_cleaning', + autospec=True) + def test_tear_down_cleaning_manage_agent_boot_false( + self, tear_down_cleaning_mock): + self.config(group='agent', manage_agent_boot=False) + with task_manager.acquire(self.context, self.node.uuid) as task: + self.driver.tear_down_cleaning(task) + tear_down_cleaning_mock.assert_called_once_with( + task, manage_boot=False) + + +class TestAgentVendor(db_base.DbTestCase): + + def setUp(self): + super(TestAgentVendor, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_agent") + self.passthru = agent.AgentVendorInterface() + n = { + 'driver': 'fake_agent', + 'instance_info': INSTANCE_INFO, + 'driver_info': DRIVER_INFO, + 'driver_internal_info': DRIVER_INTERNAL_INFO, + } + self.node = object_utils.create_test_node(self.context, **n) + + def test_continue_deploy(self): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + test_temp_url = 'http://image' + expected_image_info = { + 'urls': [test_temp_url], + 'id': 'fake-image', + 'checksum': 'checksum', + 'disk_format': 'qcow2', + 'container_format': 'bare', + } + + client_mock = mock.MagicMock(spec_set=['prepare_image']) + self.passthru._client = client_mock + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.passthru.continue_deploy(task) + + client_mock.prepare_image.assert_called_with(task.node, + expected_image_info) + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, + task.node.target_provision_state) + + def test_continue_deploy_image_source_is_url(self): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + test_temp_url = 'http://image' + expected_image_info = { + 'urls': [test_temp_url], + 'id': self.node.instance_info['image_source'], + 'checksum': 'checksum', + 'disk_format': 'qcow2', + 'container_format': 'bare', + } + + client_mock = mock.MagicMock(spec_set=['prepare_image']) + self.passthru._client = client_mock + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.passthru.continue_deploy(task) + + client_mock.prepare_image.assert_called_with(task.node, + expected_image_info) + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, + task.node.target_provision_state) + + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_power_state', + spec=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'power_off', + spec=types.FunctionType) + @mock.patch('ironic.conductor.utils.node_set_boot_device', autospec=True) + @mock.patch('ironic.drivers.modules.agent.AgentVendorInterface' + '.check_deploy_success', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True) + def test_reboot_to_instance(self, clean_pxe_mock, check_deploy_mock, + bootdev_mock, power_off_mock, + get_power_state_mock, node_power_action_mock): + check_deploy_mock.return_value = None + + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + get_power_state_mock.return_value = states.POWER_OFF + task.node.driver_internal_info['is_whole_disk_image'] = True + + self.passthru.reboot_to_instance(task) + + clean_pxe_mock.assert_called_once_with(task.driver.boot, task) + check_deploy_mock.assert_called_once_with(mock.ANY, task.node) + bootdev_mock.assert_called_once_with(task, 'disk', persistent=True) + power_off_mock.assert_called_once_with(task.node) + get_power_state_mock.assert_called_once_with(task) + node_power_action_mock.assert_called_once_with( + task, states.POWER_ON) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_power_state', + spec=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'power_off', + spec=types.FunctionType) + @mock.patch('ironic.conductor.utils.node_set_boot_device', autospec=True) + @mock.patch('ironic.drivers.modules.agent.AgentVendorInterface' + '.check_deploy_success', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True) + def test_reboot_to_instance_boot_none(self, clean_pxe_mock, + check_deploy_mock, + bootdev_mock, power_off_mock, + get_power_state_mock, + node_power_action_mock): + check_deploy_mock.return_value = None + + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + get_power_state_mock.return_value = states.POWER_OFF + task.node.driver_internal_info['is_whole_disk_image'] = True + task.driver.boot = None + + self.passthru.reboot_to_instance(task) + + self.assertFalse(clean_pxe_mock.called) + check_deploy_mock.assert_called_once_with(mock.ANY, task.node) + bootdev_mock.assert_called_once_with(task, 'disk', persistent=True) + power_off_mock.assert_called_once_with(task.node) + get_power_state_mock.assert_called_once_with(task) + node_power_action_mock.assert_called_once_with( + task, states.POWER_ON) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_has_started(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [] + self.assertFalse(self.passthru.deploy_has_started(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_has_started_is_done(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [{'command_name': 'prepare_image', + 'command_status': 'SUCCESS'}] + self.assertTrue(self.passthru.deploy_has_started(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_has_started_did_start(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [{'command_name': 'prepare_image', + 'command_status': 'RUNNING'}] + self.assertTrue(self.passthru.deploy_has_started(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_has_started_multiple_commands(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [{'command_name': 'cache_image', + 'command_status': 'SUCCESS'}, + {'command_name': 'prepare_image', + 'command_status': 'RUNNING'}] + self.assertTrue(self.passthru.deploy_has_started(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_has_started_other_commands(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [{'command_name': 'cache_image', + 'command_status': 'SUCCESS'}] + self.assertFalse(self.passthru.deploy_has_started(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_is_done(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [{'command_name': 'prepare_image', + 'command_status': 'SUCCESS'}] + self.assertTrue(self.passthru.deploy_is_done(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_is_done_empty_response(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [] + self.assertFalse(self.passthru.deploy_is_done(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_is_done_race(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [{'command_name': 'some_other_command', + 'command_status': 'SUCCESS'}] + self.assertFalse(self.passthru.deploy_is_done(task)) + + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_deploy_is_done_still_running(self, mock_get_cmd): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_get_cmd.return_value = [{'command_name': 'prepare_image', + 'command_status': 'RUNNING'}] + self.assertFalse(self.passthru.deploy_is_done(task)) + + +class AgentRAIDTestCase(db_base.DbTestCase): + + def setUp(self): + super(AgentRAIDTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_agent") + self.passthru = agent.AgentVendorInterface() + self.target_raid_config = { + "logical_disks": [ + {'size_gb': 200, 'raid_level': 0, 'is_root_volume': True}, + {'size_gb': 200, 'raid_level': 5} + ]} + self.clean_step = {'step': 'create_configuration', + 'interface': 'raid'} + n = { + 'driver': 'fake_agent', + 'instance_info': INSTANCE_INFO, + 'driver_info': DRIVER_INFO, + 'driver_internal_info': DRIVER_INTERNAL_INFO, + 'target_raid_config': self.target_raid_config, + 'clean_step': self.clean_step, + } + self.node = object_utils.create_test_node(self.context, **n) + + @mock.patch.object(deploy_utils, 'agent_get_clean_steps', autospec=True) + def test_get_clean_steps(self, get_steps_mock): + get_steps_mock.return_value = [ + {'step': 'create_configuration', 'interface': 'raid', + 'priority': 1}, + {'step': 'delete_configuration', 'interface': 'raid', + 'priority': 2}] + + with task_manager.acquire(self.context, self.node.uuid) as task: + ret = task.driver.raid.get_clean_steps(task) + + self.assertEqual(0, ret[0]['priority']) + self.assertEqual(0, ret[1]['priority']) + + @mock.patch.object(deploy_utils, 'agent_execute_clean_step', + autospec=True) + def test_create_configuration(self, execute_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + execute_mock.return_value = states.CLEANWAIT + + return_value = task.driver.raid.create_configuration(task) + + self.assertEqual(states.CLEANWAIT, return_value) + self.assertEqual( + self.target_raid_config, + task.node.driver_internal_info['target_raid_config']) + execute_mock.assert_called_once_with(task, self.clean_step) + + @mock.patch.object(deploy_utils, 'agent_execute_clean_step', + autospec=True) + def test_create_configuration_skip_root(self, execute_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + execute_mock.return_value = states.CLEANWAIT + + return_value = task.driver.raid.create_configuration( + task, create_root_volume=False) + + self.assertEqual(states.CLEANWAIT, return_value) + execute_mock.assert_called_once_with(task, self.clean_step) + exp_target_raid_config = { + "logical_disks": [ + {'size_gb': 200, 'raid_level': 5} + ]} + self.assertEqual( + exp_target_raid_config, + task.node.driver_internal_info['target_raid_config']) + + @mock.patch.object(deploy_utils, 'agent_execute_clean_step', + autospec=True) + def test_create_configuration_skip_nonroot(self, execute_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + execute_mock.return_value = states.CLEANWAIT + + return_value = task.driver.raid.create_configuration( + task, create_nonroot_volumes=False) + + self.assertEqual(states.CLEANWAIT, return_value) + execute_mock.assert_called_once_with(task, self.clean_step) + exp_target_raid_config = { + "logical_disks": [ + {'size_gb': 200, 'raid_level': 0, 'is_root_volume': True}, + ]} + self.assertEqual( + exp_target_raid_config, + task.node.driver_internal_info['target_raid_config']) + + @mock.patch.object(deploy_utils, 'agent_execute_clean_step', + autospec=True) + def test_create_configuration_no_target_raid_config_after_skipping( + self, execute_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises( + exception.MissingParameterValue, + task.driver.raid.create_configuration, + task, create_root_volume=False, + create_nonroot_volumes=False) + + self.assertFalse(execute_mock.called) + + @mock.patch.object(deploy_utils, 'agent_execute_clean_step', + autospec=True) + def test_create_configuration_empty_target_raid_config( + self, execute_mock): + execute_mock.return_value = states.CLEANING + self.node.target_raid_config = {} + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.raid.create_configuration, + task) + self.assertFalse(execute_mock.called) + + @mock.patch.object(raid, 'update_raid_info', autospec=True) + def test__create_configuration_final( + self, update_raid_info_mock): + command = {'command_result': {'clean_result': 'foo'}} + with task_manager.acquire(self.context, self.node.uuid) as task: + raid_mgmt = agent.AgentRAID + raid_mgmt._create_configuration_final(task, command) + update_raid_info_mock.assert_called_once_with(task.node, 'foo') + + @mock.patch.object(raid, 'update_raid_info', autospec=True) + def test__create_configuration_final_registered( + self, update_raid_info_mock): + self.node.clean_step = {'interface': 'raid', + 'step': 'create_configuration'} + command = {'command_result': {'clean_result': 'foo'}} + create_hook = agent_base_vendor._get_post_clean_step_hook(self.node) + with task_manager.acquire(self.context, self.node.uuid) as task: + create_hook(task, command) + update_raid_info_mock.assert_called_once_with(task.node, 'foo') + + @mock.patch.object(raid, 'update_raid_info', autospec=True) + def test__create_configuration_final_bad_command_result( + self, update_raid_info_mock): + command = {} + with task_manager.acquire(self.context, self.node.uuid) as task: + raid_mgmt = agent.AgentRAID + self.assertRaises(exception.IronicException, + raid_mgmt._create_configuration_final, + task, command) + self.assertFalse(update_raid_info_mock.called) + + @mock.patch.object(deploy_utils, 'agent_execute_clean_step', + autospec=True) + def test_delete_configuration(self, execute_mock): + execute_mock.return_value = states.CLEANING + with task_manager.acquire(self.context, self.node.uuid) as task: + return_value = task.driver.raid.delete_configuration(task) + + execute_mock.assert_called_once_with(task, self.clean_step) + self.assertEqual(states.CLEANING, return_value) + + def test__delete_configuration_final(self): + command = {'command_result': {'clean_result': 'foo'}} + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.raid_config = {'foo': 'bar'} + raid_mgmt = agent.AgentRAID + raid_mgmt._delete_configuration_final(task, command) + + self.node.refresh() + self.assertEqual({}, self.node.raid_config) + + def test__delete_configuration_final_registered( + self): + self.node.clean_step = {'interface': 'raid', + 'step': 'delete_configuration'} + self.node.raid_config = {'foo': 'bar'} + command = {'command_result': {'clean_result': 'foo'}} + delete_hook = agent_base_vendor._get_post_clean_step_hook(self.node) + with task_manager.acquire(self.context, self.node.uuid) as task: + delete_hook(task, command) + + self.node.refresh() + self.assertEqual({}, self.node.raid_config) diff --git a/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py new file mode 100644 index 000000000..2cf23ab3e --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py @@ -0,0 +1,948 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2015 Red Hat, Inc. +# 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 time +import types + +import mock + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common import states +from ironic.conductor import manager +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import agent_base_vendor +from ironic.drivers.modules import agent_client +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules import fake +from ironic import objects +from ironic.tests.unit.conductor import utils as mgr_utils +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 object_utils + +INSTANCE_INFO = db_utils.get_test_agent_instance_info() +DRIVER_INFO = db_utils.get_test_agent_driver_info() +DRIVER_INTERNAL_INFO = db_utils.get_test_agent_driver_internal_info() + + +class TestBaseAgentVendor(db_base.DbTestCase): + + def setUp(self): + super(TestBaseAgentVendor, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_agent") + self.passthru = agent_base_vendor.BaseAgentVendor() + n = { + 'driver': 'fake_agent', + 'instance_info': INSTANCE_INFO, + 'driver_info': DRIVER_INFO, + 'driver_internal_info': DRIVER_INTERNAL_INFO, + } + self.node = object_utils.create_test_node(self.context, **n) + + def test_validate(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + method = 'heartbeat' + self.passthru.validate(task, method) + + def test_driver_validate(self): + kwargs = {'version': '2'} + method = 'lookup' + self.passthru.driver_validate(method, **kwargs) + + def test_driver_validate_invalid_paremeter(self): + method = 'lookup' + kwargs = {'version': '1'} + self.assertRaises(exception.InvalidParameterValue, + self.passthru.driver_validate, + method, **kwargs) + + def test_driver_validate_missing_parameter(self): + method = 'lookup' + kwargs = {} + self.assertRaises(exception.MissingParameterValue, + self.passthru.driver_validate, + method, **kwargs) + + def test_lookup_version_not_found(self): + kwargs = { + 'version': '999', + } + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + self.passthru.lookup, + task.context, + **kwargs) + + @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' + '._find_node_by_macs', autospec=True) + def test_lookup_v2(self, find_mock): + kwargs = { + 'version': '2', + 'inventory': { + 'interfaces': [ + { + 'mac_address': 'aa:bb:cc:dd:ee:ff', + 'name': 'eth0' + }, + { + 'mac_address': 'ff:ee:dd:cc:bb:aa', + 'name': 'eth1' + } + + ] + } + } + find_mock.return_value = self.node + with task_manager.acquire(self.context, self.node.uuid) as task: + node = self.passthru.lookup(task.context, **kwargs) + self.assertEqual(self.node.as_dict(), node['node']) + + def test_lookup_v2_missing_inventory(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + self.passthru.lookup, + task.context) + + def test_lookup_v2_empty_inventory(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + self.passthru.lookup, + task.context, + inventory={}) + + def test_lookup_v2_empty_interfaces(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.NodeNotFound, + self.passthru.lookup, + task.context, + version='2', + inventory={'interfaces': []}) + + @mock.patch.object(objects.Node, 'get_by_uuid') + def test_lookup_v2_with_node_uuid(self, mock_get_node): + kwargs = { + 'version': '2', + 'node_uuid': 'fake uuid', + 'inventory': { + 'interfaces': [ + { + 'mac_address': 'aa:bb:cc:dd:ee:ff', + 'name': 'eth0' + }, + { + 'mac_address': 'ff:ee:dd:cc:bb:aa', + 'name': 'eth1' + } + + ] + } + } + mock_get_node.return_value = self.node + with task_manager.acquire(self.context, self.node.uuid) as task: + node = self.passthru.lookup(task.context, **kwargs) + self.assertEqual(self.node.as_dict(), node['node']) + mock_get_node.assert_called_once_with(mock.ANY, 'fake uuid') + + @mock.patch.object(objects.port.Port, 'get_by_address', + spec_set=types.FunctionType) + def test_find_ports_by_macs(self, mock_get_port): + fake_port = object_utils.get_test_port(self.context) + mock_get_port.return_value = fake_port + + macs = ['aa:bb:cc:dd:ee:ff'] + + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + ports = self.passthru._find_ports_by_macs(task, macs) + self.assertEqual(1, len(ports)) + self.assertEqual(fake_port.uuid, ports[0].uuid) + self.assertEqual(fake_port.node_id, ports[0].node_id) + + @mock.patch.object(objects.port.Port, 'get_by_address', + spec_set=types.FunctionType) + def test_find_ports_by_macs_bad_params(self, mock_get_port): + mock_get_port.side_effect = exception.PortNotFound(port="123") + + macs = ['aa:bb:cc:dd:ee:ff'] + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + empty_ids = self.passthru._find_ports_by_macs(task, macs) + self.assertEqual([], empty_ids) + + @mock.patch('ironic.objects.node.Node.get_by_id', + spec_set=types.FunctionType) + @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' + '._get_node_id', autospec=True) + @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' + '._find_ports_by_macs', autospec=True) + def test_find_node_by_macs(self, ports_mock, node_id_mock, node_mock): + ports_mock.return_value = object_utils.get_test_port(self.context) + node_id_mock.return_value = '1' + node_mock.return_value = self.node + + macs = ['aa:bb:cc:dd:ee:ff'] + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + node = self.passthru._find_node_by_macs(task, macs) + self.assertEqual(node, node) + + @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' + '._find_ports_by_macs', autospec=True) + def test_find_node_by_macs_no_ports(self, ports_mock): + ports_mock.return_value = [] + + macs = ['aa:bb:cc:dd:ee:ff'] + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + self.assertRaises(exception.NodeNotFound, + self.passthru._find_node_by_macs, + task, + macs) + + @mock.patch('ironic.objects.node.Node.get_by_uuid', + spec_set=types.FunctionType) + @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' + '._get_node_id', autospec=True) + @mock.patch('ironic.drivers.modules.agent_base_vendor.BaseAgentVendor' + '._find_ports_by_macs', autospec=True) + def test_find_node_by_macs_nodenotfound(self, ports_mock, node_id_mock, + node_mock): + port = object_utils.get_test_port(self.context) + ports_mock.return_value = [port] + node_id_mock.return_value = self.node['uuid'] + node_mock.side_effect = [self.node, + exception.NodeNotFound(node=self.node)] + + macs = ['aa:bb:cc:dd:ee:ff'] + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + self.assertRaises(exception.NodeNotFound, + self.passthru._find_node_by_macs, + task, + macs) + + def test_get_node_id(self): + fake_port1 = object_utils.get_test_port(self.context, + node_id=123, + address="aa:bb:cc:dd:ee:fe") + fake_port2 = object_utils.get_test_port(self.context, + node_id=123, + id=42, + address="aa:bb:cc:dd:ee:fb", + uuid='1be26c0b-03f2-4d2e-ae87-' + 'c02d7f33c782') + + node_id = self.passthru._get_node_id([fake_port1, fake_port2]) + self.assertEqual(fake_port2.node_id, node_id) + + def test_get_node_id_exception(self): + fake_port1 = object_utils.get_test_port(self.context, + node_id=123, + address="aa:bb:cc:dd:ee:fc") + fake_port2 = object_utils.get_test_port(self.context, + node_id=321, + id=42, + address="aa:bb:cc:dd:ee:fd", + uuid='1be26c0b-03f2-4d2e-ae87-' + 'c02d7f33c782') + + self.assertRaises(exception.NodeNotFound, + self.passthru._get_node_id, + [fake_port1, fake_port2]) + + def test_get_interfaces(self): + fake_inventory = { + 'interfaces': [ + { + 'mac_address': 'aa:bb:cc:dd:ee:ff', + 'name': 'eth0' + } + ] + } + interfaces = self.passthru._get_interfaces(fake_inventory) + self.assertEqual(fake_inventory['interfaces'], interfaces) + + def test_get_interfaces_bad(self): + self.assertRaises(exception.InvalidParameterValue, + self.passthru._get_interfaces, + inventory={}) + + def test_heartbeat(self): + kwargs = { + 'agent_url': 'http://127.0.0.1:9999/bar' + } + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + self.passthru.heartbeat(task, **kwargs) + + def test_heartbeat_bad(self): + kwargs = {} + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + self.passthru.heartbeat, task, **kwargs) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, 'deploy_has_started', + autospec=True) + @mock.patch.object(deploy_utils, 'set_failed_state', autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, 'deploy_is_done', + autospec=True) + @mock.patch.object(agent_base_vendor.LOG, 'exception', autospec=True) + def test_heartbeat_deploy_done_fails(self, log_mock, done_mock, + failed_mock, deploy_started_mock): + deploy_started_mock.return_value = True + kwargs = { + 'agent_url': 'http://127.0.0.1:9999/bar' + } + done_mock.side_effect = iter([Exception('LlamaException')]) + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + task.node.provision_state = states.DEPLOYWAIT + task.node.target_provision_state = states.ACTIVE + self.passthru.heartbeat(task, **kwargs) + failed_mock.assert_called_once_with(task, mock.ANY) + log_mock.assert_called_once_with( + 'Asynchronous exception for node ' + '1be26c0b-03f2-4d2e-ae87-c02d7f33c123: Failed checking if deploy ' + 'is done. exception: LlamaException') + + @mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True) + @mock.patch.object(manager, 'set_node_cleaning_steps', autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + def test_heartbeat_resume_clean(self, mock_notify, mock_set_steps, + mock_touch): + kwargs = { + 'agent_url': 'http://127.0.0.1:9999/bar' + } + self.node.clean_step = {} + for state in (states.CLEANWAIT, states.CLEANING): + self.node.provision_state = state + self.node.save() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.passthru.heartbeat(task, **kwargs) + + mock_touch.assert_called_once_with(mock.ANY) + mock_notify.assert_called_once_with(mock.ANY, task) + mock_set_steps.assert_called_once_with(task) + # Reset mocks for the next interaction + mock_touch.reset_mock() + mock_notify.reset_mock() + mock_set_steps.reset_mock() + + @mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'continue_cleaning', autospec=True) + def test_heartbeat_continue_cleaning(self, mock_continue, mock_touch): + kwargs = { + 'agent_url': 'http://127.0.0.1:9999/bar' + } + self.node.clean_step = { + 'priority': 10, + 'interface': 'deploy', + 'step': 'foo', + 'reboot_requested': False + } + for state in (states.CLEANWAIT, states.CLEANING): + self.node.provision_state = state + self.node.save() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.passthru.heartbeat(task, **kwargs) + + mock_touch.assert_called_once_with(mock.ANY) + mock_continue.assert_called_once_with(mock.ANY, task, **kwargs) + # Reset mocks for the next interaction + mock_touch.reset_mock() + mock_continue.reset_mock() + + @mock.patch('ironic.conductor.manager.cleaning_error_handler') + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'continue_cleaning', autospec=True) + def test_heartbeat_continue_cleaning_fails(self, mock_continue, + mock_handler): + kwargs = { + 'agent_url': 'http://127.0.0.1:9999/bar' + } + self.node.clean_step = { + 'priority': 10, + 'interface': 'deploy', + 'step': 'foo', + 'reboot_requested': False + } + + mock_continue.side_effect = Exception() + + for state in (states.CLEANWAIT, states.CLEANING): + self.node.provision_state = state + self.node.save() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.passthru.heartbeat(task, **kwargs) + + mock_continue.assert_called_once_with(mock.ANY, task, **kwargs) + mock_handler.assert_called_once_with(task, mock.ANY) + mock_handler.reset_mock() + mock_continue.reset_mock() + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, 'continue_deploy', + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, 'reboot_to_instance', + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + def test_heartbeat_noops_maintenance_mode(self, ncrc_mock, rti_mock, + cd_mock): + """Ensures that heartbeat() no-ops for a maintenance node.""" + kwargs = { + 'agent_url': 'http://127.0.0.1:9999/bar' + } + self.node.maintenance = True + for state in (states.AVAILABLE, states.DEPLOYWAIT, states.DEPLOYING, + states.CLEANING): + self.node.provision_state = state + self.node.save() + with task_manager.acquire( + self.context, self.node['uuid'], shared=True) as task: + self.passthru.heartbeat(task, **kwargs) + + self.assertEqual(0, ncrc_mock.call_count) + self.assertEqual(0, rti_mock.call_count) + self.assertEqual(0, cd_mock.call_count) + + @mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, 'deploy_has_started', + autospec=True) + def test_heartbeat_touch_provisioning(self, mock_deploy_started, + mock_touch): + mock_deploy_started.return_value = True + kwargs = { + 'agent_url': 'http://127.0.0.1:9999/bar' + } + + self.node.provision_state = states.DEPLOYWAIT + self.node.save() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.passthru.heartbeat(task, **kwargs) + + mock_touch.assert_called_once_with(mock.ANY) + + def test_vendor_passthru_vendor_routes(self): + expected = ['heartbeat'] + 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(expected, list(vendor_routes)) + + def test_vendor_passthru_driver_routes(self): + expected = ['lookup'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + driver_routes = task.driver.vendor.driver_routes + self.assertIsInstance(driver_routes, dict) + self.assertEqual(expected, list(driver_routes)) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_power_state', + spec=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'power_off', + spec=types.FunctionType) + def test_reboot_and_finish_deploy(self, power_off_mock, + get_power_state_mock, + node_power_action_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + get_power_state_mock.side_effect = [states.POWER_ON, + states.POWER_OFF] + self.passthru.reboot_and_finish_deploy(task) + power_off_mock.assert_called_once_with(task.node) + self.assertEqual(2, get_power_state_mock.call_count) + node_power_action_mock.assert_called_once_with( + task, states.POWER_ON) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_power_state', + spec=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'power_off', + spec=types.FunctionType) + def test_reboot_and_finish_deploy_soft_poweroff_doesnt_complete( + self, power_off_mock, get_power_state_mock, + node_power_action_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + get_power_state_mock.return_value = states.POWER_ON + self.passthru.reboot_and_finish_deploy(task) + power_off_mock.assert_called_once_with(task.node) + self.assertEqual(7, get_power_state_mock.call_count) + node_power_action_mock.assert_called_once_with( + task, states.REBOOT) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'power_off', + spec=types.FunctionType) + def test_reboot_and_finish_deploy_soft_poweroff_fails( + self, power_off_mock, node_power_action_mock): + power_off_mock.side_effect = iter([RuntimeError("boom")]) + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.passthru.reboot_and_finish_deploy(task) + power_off_mock.assert_called_once_with(task.node) + node_power_action_mock.assert_called_once_with( + task, states.REBOOT) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_power_state', + spec=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'power_off', + spec=types.FunctionType) + def test_reboot_and_finish_deploy_get_power_state_fails( + self, power_off_mock, get_power_state_mock, + node_power_action_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + get_power_state_mock.side_effect = iter([RuntimeError("boom")]) + self.passthru.reboot_and_finish_deploy(task) + power_off_mock.assert_called_once_with(task.node) + self.assertEqual(7, get_power_state_mock.call_count) + node_power_action_mock.assert_called_once_with( + task, states.REBOOT) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(time, 'sleep', lambda seconds: None) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_power_state', + spec=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'power_off', + spec=types.FunctionType) + def test_reboot_and_finish_deploy_power_action_fails( + self, power_off_mock, get_power_state_mock, + node_power_action_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + get_power_state_mock.return_value = states.POWER_ON + node_power_action_mock.side_effect = iter([RuntimeError("boom")]) + self.assertRaises(exception.InstanceDeployFailure, + self.passthru.reboot_and_finish_deploy, + task) + power_off_mock.assert_called_once_with(task.node) + self.assertEqual(7, get_power_state_mock.call_count) + node_power_action_mock.assert_has_calls([ + mock.call(task, states.REBOOT), + mock.call(task, states.POWER_OFF)]) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', + autospec=True) + @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True) + def test_configure_local_boot(self, try_set_boot_device_mock, + install_bootloader_mock): + install_bootloader_mock.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = False + self.passthru.configure_local_boot(task, + root_uuid='some-root-uuid') + try_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK) + install_bootloader_mock.assert_called_once_with( + mock.ANY, task.node, root_uuid='some-root-uuid', + efi_system_part_uuid=None) + + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', + autospec=True) + @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True) + def test_configure_local_boot_uefi(self, try_set_boot_device_mock, + install_bootloader_mock): + install_bootloader_mock.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = False + self.passthru.configure_local_boot( + task, root_uuid='some-root-uuid', + efi_system_part_uuid='efi-system-part-uuid') + try_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK) + install_bootloader_mock.assert_called_once_with( + mock.ANY, task.node, root_uuid='some-root-uuid', + efi_system_part_uuid='efi-system-part-uuid') + + @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', + autospec=True) + def test_configure_local_boot_whole_disk_image( + self, install_bootloader_mock, try_set_boot_device_mock): + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.configure_local_boot(task) + self.assertFalse(install_bootloader_mock.called) + try_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK) + + @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', + autospec=True) + def test_configure_local_boot_no_root_uuid( + self, install_bootloader_mock, try_set_boot_device_mock): + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = False + self.passthru.configure_local_boot(task) + self.assertFalse(install_bootloader_mock.called) + try_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK) + + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', + autospec=True) + def test_configure_local_boot_boot_loader_install_fail( + self, install_bootloader_mock): + install_bootloader_mock.return_value = { + 'command_status': 'FAILED', 'command_error': 'boom'} + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = False + self.assertRaises(exception.InstanceDeployFailure, + self.passthru.configure_local_boot, + task, root_uuid='some-root-uuid') + install_bootloader_mock.assert_called_once_with( + mock.ANY, task.node, root_uuid='some-root-uuid', + efi_system_part_uuid=None) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + + @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', + autospec=True) + def test_configure_local_boot_set_boot_device_fail( + self, install_bootloader_mock, try_set_boot_device_mock): + install_bootloader_mock.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + try_set_boot_device_mock.side_effect = iter([RuntimeError('error')]) + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = False + self.assertRaises(exception.InstanceDeployFailure, + self.passthru.configure_local_boot, + task, root_uuid='some-root-uuid') + install_bootloader_mock.assert_called_once_with( + mock.ANY, task.node, root_uuid='some-root-uuid', + efi_system_part_uuid=None) + try_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning(self, status_mock, notify_mock): + # Test a successful execute clean step on the agent + self.node.clean_step = { + 'priority': 10, + 'interface': 'deploy', + 'step': 'erase_devices', + 'reboot_requested': False + } + self.node.save() + status_mock.return_value = [{ + 'command_status': 'SUCCEEDED', + 'command_name': 'execute_clean_step', + 'command_result': { + 'clean_step': self.node.clean_step + } + }] + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.continue_cleaning(task) + notify_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(agent_base_vendor, + '_get_post_clean_step_hook', autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning_with_hook( + self, status_mock, notify_mock, get_hook_mock): + self.node.clean_step = { + 'priority': 10, + 'interface': 'raid', + 'step': 'create_configuration', + } + self.node.save() + command_status = { + 'command_status': 'SUCCEEDED', + 'command_name': 'execute_clean_step', + 'command_result': {'clean_step': self.node.clean_step}} + status_mock.return_value = [command_status] + hook_mock = mock.MagicMock(spec=types.FunctionType, __name__='foo') + get_hook_mock.return_value = hook_mock + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.passthru.continue_cleaning(task) + + get_hook_mock.assert_called_once_with(task.node) + hook_mock.assert_called_once_with(task, command_status) + notify_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + @mock.patch.object(agent_base_vendor, + '_get_post_clean_step_hook', autospec=True) + @mock.patch.object(manager, 'cleaning_error_handler', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning_with_hook_fails( + self, status_mock, error_handler_mock, get_hook_mock, + notify_mock): + self.node.clean_step = { + 'priority': 10, + 'interface': 'raid', + 'step': 'create_configuration', + } + self.node.save() + command_status = { + 'command_status': 'SUCCEEDED', + 'command_name': 'execute_clean_step', + 'command_result': {'clean_step': self.node.clean_step}} + status_mock.return_value = [command_status] + hook_mock = mock.MagicMock(spec=types.FunctionType, __name__='foo') + hook_mock.side_effect = RuntimeError('error') + get_hook_mock.return_value = hook_mock + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.passthru.continue_cleaning(task) + + get_hook_mock.assert_called_once_with(task.node) + hook_mock.assert_called_once_with(task, command_status) + error_handler_mock.assert_called_once_with(task, mock.ANY) + self.assertFalse(notify_mock.called) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning_old_command(self, status_mock, notify_mock): + # Test when a second execute_clean_step happens to the agent, but + # the new step hasn't started yet. + self.node.clean_step = { + 'priority': 10, + 'interface': 'deploy', + 'step': 'erase_devices', + 'reboot_requested': False + } + self.node.save() + status_mock.return_value = [{ + 'command_status': 'SUCCEEDED', + 'command_name': 'execute_clean_step', + 'command_result': { + 'priority': 20, + 'interface': 'deploy', + 'step': 'update_firmware', + 'reboot_requested': False + } + }] + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.continue_cleaning(task) + self.assertFalse(notify_mock.called) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning_running(self, status_mock, notify_mock): + # Test that no action is taken while a clean step is executing + status_mock.return_value = [{ + 'command_status': 'RUNNING', + 'command_name': 'execute_clean_step', + 'command_result': None + }] + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.continue_cleaning(task) + self.assertFalse(notify_mock.called) + + @mock.patch('ironic.conductor.manager.cleaning_error_handler', + autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning_fail(self, status_mock, error_mock): + # Test the a failure puts the node in CLEANFAIL + status_mock.return_value = [{ + 'command_status': 'FAILED', + 'command_name': 'execute_clean_step', + 'command_result': {} + }] + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.continue_cleaning(task) + error_mock.assert_called_once_with(task, mock.ANY) + + @mock.patch('ironic.conductor.manager.set_node_cleaning_steps', + autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + '_notify_conductor_resume_clean', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning_clean_version_mismatch( + self, status_mock, notify_mock, steps_mock): + # Test that cleaning is restarted if there is a version mismatch + status_mock.return_value = [{ + 'command_status': 'CLEAN_VERSION_MISMATCH', + 'command_name': 'execute_clean_step', + 'command_result': {} + }] + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.continue_cleaning(task) + steps_mock.assert_called_once_with(task) + notify_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch('ironic.conductor.manager.cleaning_error_handler', + autospec=True) + @mock.patch.object(agent_client.AgentClient, 'get_commands_status', + autospec=True) + def test_continue_cleaning_unknown(self, status_mock, error_mock): + # Test that unknown commands are treated as failures + status_mock.return_value = [{ + 'command_status': 'UNKNOWN', + 'command_name': 'execute_clean_step', + 'command_result': {} + }] + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.passthru.continue_cleaning(task) + error_mock.assert_called_once_with(task, mock.ANY) + + def _test_clean_step_hook(self, hook_dict_mock): + """Helper method for unit tests related to clean step hooks. + + This is a helper method for other unit tests related to + clean step hooks. It acceps a mock 'hook_dict_mock' which is + a MagicMock and sets it up to function as a mock dictionary. + After that, it defines a dummy hook_method for two clean steps + raid.create_configuration and raid.delete_configuration. + + :param hook_dict_mock: An instance of mock.MagicMock() which + is the mocked value of agent_base_vendor.POST_CLEAN_STEP_HOOKS + :returns: a tuple, where the first item is the hook method created + by this method and second item is the backend dictionary for + the mocked hook_dict_mock + """ + hook_dict = {} + + def get(key, default): + return hook_dict.get(key, default) + + def getitem(self, key): + return hook_dict[key] + + def setdefault(key, default): + if key not in hook_dict: + hook_dict[key] = default + return hook_dict[key] + + hook_dict_mock.get = get + hook_dict_mock.__getitem__ = getitem + hook_dict_mock.setdefault = setdefault + some_function_mock = mock.MagicMock() + + @agent_base_vendor.post_clean_step_hook( + interface='raid', step='delete_configuration') + @agent_base_vendor.post_clean_step_hook( + interface='raid', step='create_configuration') + def hook_method(): + some_function_mock('some-arguments') + + return hook_method, hook_dict + + @mock.patch.object(agent_base_vendor, 'POST_CLEAN_STEP_HOOKS', + spec_set=dict) + def test_post_clean_step_hook(self, hook_dict_mock): + # This unit test makes sure that hook methods are registered + # properly and entries are made in + # agent_base_vendor.POST_CLEAN_STEP_HOOKS + hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock) + self.assertEqual(hook_method, + hook_dict['raid']['create_configuration']) + self.assertEqual(hook_method, + hook_dict['raid']['delete_configuration']) + + @mock.patch.object(agent_base_vendor, 'POST_CLEAN_STEP_HOOKS', + spec_set=dict) + def test__get_post_clean_step_hook(self, hook_dict_mock): + # Check if agent_base_vendor._get_post_clean_step_hook can get + # clean step for which hook is registered. + hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock) + self.node.clean_step = {'step': 'create_configuration', + 'interface': 'raid'} + self.node.save() + hook_returned = agent_base_vendor._get_post_clean_step_hook(self.node) + self.assertEqual(hook_method, hook_returned) + + @mock.patch.object(agent_base_vendor, 'POST_CLEAN_STEP_HOOKS', + spec_set=dict) + def test__get_post_clean_step_hook_no_hook_registered( + self, hook_dict_mock): + # Make sure agent_base_vendor._get_post_clean_step_hook returns + # None when no clean step hook is registered for the clean step. + hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock) + self.node.clean_step = {'step': 'some-clean-step', + 'interface': 'some-other-interface'} + self.node.save() + hook_returned = agent_base_vendor._get_post_clean_step_hook(self.node) + self.assertIsNone(hook_returned) diff --git a/ironic/tests/unit/drivers/modules/test_agent_client.py b/ironic/tests/unit/drivers/modules/test_agent_client.py new file mode 100644 index 000000000..223844882 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_agent_client.py @@ -0,0 +1,220 @@ +# Copyright 2014 Rackspace, Inc. +# +# 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 json + +import mock +import requests +import six + +from ironic.common import exception +from ironic.drivers.modules import agent_client +from ironic.tests.unit import base + + +class MockResponse(object): + status_code = 200 + + def __init__(self, text): + assert isinstance(text, six.string_types) + self.text = text + + def json(self): + return json.loads(self.text) + + +class MockNode(object): + def __init__(self): + self.uuid = 'uuid' + self.driver_info = {} + self.driver_internal_info = { + 'agent_url': "http://127.0.0.1:9999", + 'clean_version': {'generic': '1'} + } + self.instance_info = {} + + def as_dict(self): + return { + 'uuid': self.uuid, + 'driver_info': self.driver_info, + 'driver_internal_info': self.driver_internal_info, + 'instance_info': self.instance_info + } + + +class TestAgentClient(base.TestCase): + def setUp(self): + super(TestAgentClient, self).setUp() + self.client = agent_client.AgentClient() + self.client.session = mock.MagicMock(autospec=requests.Session) + self.node = MockNode() + + def test_content_type_header(self): + client = agent_client.AgentClient() + self.assertEqual('application/json', + client.session.headers['Content-Type']) + + def test__get_command_url(self): + command_url = self.client._get_command_url(self.node) + expected = self.node.driver_internal_info['agent_url'] + '/v1/commands' + self.assertEqual(expected, command_url) + + def test__get_command_url_fail(self): + del self.node.driver_internal_info['agent_url'] + self.assertRaises(exception.IronicException, + self.client._get_command_url, + self.node) + + def test__get_command_body(self): + expected = json.dumps({'name': 'prepare_image', 'params': {}}) + self.assertEqual(expected, + self.client._get_command_body('prepare_image', {})) + + def test__command(self): + response_data = {'status': 'ok'} + response_text = json.dumps(response_data) + self.client.session.post.return_value = MockResponse(response_text) + method = 'standby.run_image' + image_info = {'image_id': 'test_image'} + params = {'image_info': image_info} + + url = self.client._get_command_url(self.node) + body = self.client._get_command_body(method, params) + + response = self.client._command(self.node, method, params) + self.assertEqual(response, response_data) + self.client.session.post.assert_called_once_with( + url, + data=body, + params={'wait': 'false'}) + + def test__command_fail_json(self): + response_text = 'this be not json matey!' + self.client.session.post.return_value = MockResponse(response_text) + method = 'standby.run_image' + image_info = {'image_id': 'test_image'} + params = {'image_info': image_info} + + url = self.client._get_command_url(self.node) + body = self.client._get_command_body(method, params) + + self.assertRaises(exception.IronicException, + self.client._command, + self.node, method, params) + self.client.session.post.assert_called_once_with( + url, + data=body, + params={'wait': 'false'}) + + def test_get_commands_status(self): + with mock.patch.object(self.client.session, 'get', + autospec=True) as mock_get: + res = mock.MagicMock(spec_set=['json']) + res.json.return_value = {'commands': []} + mock_get.return_value = res + self.assertEqual([], self.client.get_commands_status(self.node)) + + @mock.patch('uuid.uuid4', mock.MagicMock(spec_set=[], return_value='uuid')) + def test_prepare_image(self): + self.client._command = mock.MagicMock(spec_set=[]) + image_info = {'image_id': 'image'} + params = {'image_info': image_info} + + self.client.prepare_image(self.node, + image_info, + wait=False) + self.client._command.assert_called_once_with( + node=self.node, method='standby.prepare_image', + params=params, wait=False) + + @mock.patch('uuid.uuid4', mock.MagicMock(spec_set=[], return_value='uuid')) + def test_prepare_image_with_configdrive(self): + self.client._command = mock.MagicMock(spec_set=[]) + configdrive_url = 'http://swift/configdrive' + self.node.instance_info['configdrive'] = configdrive_url + image_info = {'image_id': 'image'} + params = { + 'image_info': image_info, + 'configdrive': configdrive_url, + } + + self.client.prepare_image(self.node, + image_info, + wait=False) + self.client._command.assert_called_once_with( + node=self.node, method='standby.prepare_image', + params=params, wait=False) + + @mock.patch('uuid.uuid4', mock.MagicMock(spec_set=[], return_value='uuid')) + def test_start_iscsi_target(self): + self.client._command = mock.MagicMock(spec_set=[]) + iqn = 'fake-iqn' + params = {'iqn': iqn} + + self.client.start_iscsi_target(self.node, iqn) + self.client._command.assert_called_once_with( + node=self.node, method='iscsi.start_iscsi_target', + params=params, wait=True) + + @mock.patch('uuid.uuid4', mock.MagicMock(spec_set=[], return_value='uuid')) + def test_install_bootloader(self): + self.client._command = mock.MagicMock(spec_set=[]) + root_uuid = 'fake-root-uuid' + efi_system_part_uuid = 'fake-efi-system-part-uuid' + params = {'root_uuid': root_uuid, + 'efi_system_part_uuid': efi_system_part_uuid} + + self.client.install_bootloader( + self.node, root_uuid, efi_system_part_uuid=efi_system_part_uuid) + self.client._command.assert_called_once_with( + node=self.node, method='image.install_bootloader', params=params, + wait=True) + + def test_get_clean_steps(self): + self.client._command = mock.MagicMock(spec_set=[]) + ports = [] + expected_params = { + 'node': self.node.as_dict(), + 'ports': [] + } + + self.client.get_clean_steps(self.node, + ports) + self.client._command.assert_called_once_with( + node=self.node, method='clean.get_clean_steps', + params=expected_params, wait=True) + + def test_execute_clean_step(self): + self.client._command = mock.MagicMock(spec_set=[]) + ports = [] + step = {'priority': 10, 'step': 'erase_devices', 'interface': 'deploy'} + expected_params = { + 'step': step, + 'node': self.node.as_dict(), + 'ports': [], + 'clean_version': self.node.driver_internal_info.get( + 'hardware_manager_version') + } + self.client.execute_clean_step(step, + self.node, + ports) + self.client._command.assert_called_once_with( + node=self.node, method='clean.execute_clean_step', + params=expected_params, wait=False) + + def test_power_off(self): + self.client._command = mock.MagicMock(spec_set=[]) + self.client.power_off(self.node) + self.client._command.assert_called_once_with( + node=self.node, method='standby.power_off', params={}) diff --git a/ironic/tests/unit/drivers/modules/test_console_utils.py b/ironic/tests/unit/drivers/modules/test_console_utils.py new file mode 100644 index 000000000..23762e9f8 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_console_utils.py @@ -0,0 +1,348 @@ +# coding=utf-8 + +# Copyright 2014 International Business Machines Corporation +# 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. + +"""Test class for console_utils driver module.""" + +import os +import random +import string +import subprocess +import tempfile + +import mock +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_utils import netutils + +from ironic.common import exception +from ironic.common import utils +from ironic.drivers.modules import console_utils +from ironic.drivers.modules import ipmitool as ipmi +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 + + +CONF = cfg.CONF + +INFO_DICT = db_utils.get_test_ipmi_info() + + +class ConsoleUtilsTestCase(db_base.DbTestCase): + + def setUp(self): + super(ConsoleUtilsTestCase, self).setUp() + self.node = obj_utils.get_test_node( + self.context, + driver='fake_ipmitool', + driver_info=INFO_DICT) + self.info = ipmi._parse_driver_info(self.node) + + def test__get_console_pid_dir(self): + pid_dir = '/tmp/pid_dir' + self.config(terminal_pid_dir=pid_dir, group='console') + dir = console_utils._get_console_pid_dir() + self.assertEqual(pid_dir, dir) + + def test__get_console_pid_dir_tempdir(self): + self.config(tempdir='/tmp/fake_dir') + dir = console_utils._get_console_pid_dir() + self.assertEqual(CONF.tempdir, dir) + + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(os.path, 'exists', autospec=True) + def test__ensure_console_pid_dir_exists(self, mock_path_exists, + mock_makedirs): + mock_path_exists.return_value = True + mock_makedirs.side_effect = OSError + pid_dir = console_utils._get_console_pid_dir() + + console_utils._ensure_console_pid_dir_exists() + + mock_path_exists.assert_called_once_with(pid_dir) + self.assertFalse(mock_makedirs.called) + + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(os.path, 'exists', autospec=True) + def test__ensure_console_pid_dir_exists_fail(self, mock_path_exists, + mock_makedirs): + mock_path_exists.return_value = False + mock_makedirs.side_effect = OSError + pid_dir = console_utils._get_console_pid_dir() + + self.assertRaises(exception.ConsoleError, + console_utils._ensure_console_pid_dir_exists) + + mock_path_exists.assert_called_once_with(pid_dir) + mock_makedirs.assert_called_once_with(pid_dir) + + @mock.patch.object(console_utils, '_get_console_pid_dir', autospec=True) + def test__get_console_pid_file(self, mock_dir): + mock_dir.return_value = tempfile.gettempdir() + expected_path = '%(tempdir)s/%(uuid)s.pid' % { + 'tempdir': mock_dir.return_value, + 'uuid': self.info.get('uuid')} + path = console_utils._get_console_pid_file(self.info['uuid']) + self.assertEqual(expected_path, path) + mock_dir.assert_called_once_with() + + @mock.patch.object(console_utils, '_get_console_pid_file', autospec=True) + def test__get_console_pid(self, mock_exec): + tmp_file_handle = tempfile.NamedTemporaryFile() + tmp_file = tmp_file_handle.name + self.addCleanup(utils.unlink_without_raise, tmp_file) + with open(tmp_file, "w") as f: + f.write("12345\n") + + mock_exec.return_value = tmp_file + + pid = console_utils._get_console_pid(self.info['uuid']) + + mock_exec.assert_called_once_with(self.info['uuid']) + self.assertEqual(pid, 12345) + + @mock.patch.object(console_utils, '_get_console_pid_file', autospec=True) + def test__get_console_pid_not_a_num(self, mock_exec): + tmp_file_handle = tempfile.NamedTemporaryFile() + tmp_file = tmp_file_handle.name + self.addCleanup(utils.unlink_without_raise, tmp_file) + with open(tmp_file, "w") as f: + f.write("Hello World\n") + + mock_exec.return_value = tmp_file + + self.assertRaises(exception.NoConsolePid, + console_utils._get_console_pid, + self.info['uuid']) + mock_exec.assert_called_once_with(self.info['uuid']) + + def test__get_console_pid_file_not_found(self): + self.assertRaises(exception.NoConsolePid, + console_utils._get_console_pid, + self.info['uuid']) + + @mock.patch.object(utils, 'unlink_without_raise', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + @mock.patch.object(console_utils, '_get_console_pid', autospec=True) + def test__stop_console(self, mock_pid, mock_execute, mock_unlink): + pid_file = console_utils._get_console_pid_file(self.info['uuid']) + mock_pid.return_value = '12345' + + console_utils._stop_console(self.info['uuid']) + + mock_pid.assert_called_once_with(self.info['uuid']) + mock_execute.assert_called_once_with('kill', mock_pid.return_value, + check_exit_code=[0, 99]) + mock_unlink.assert_called_once_with(pid_file) + + @mock.patch.object(utils, 'unlink_without_raise', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + @mock.patch.object(console_utils, '_get_console_pid', autospec=True) + def test__stop_console_nopid(self, mock_pid, mock_execute, mock_unlink): + pid_file = console_utils._get_console_pid_file(self.info['uuid']) + mock_pid.side_effect = iter( + [exception.NoConsolePid(pid_path="/tmp/blah")]) + + self.assertRaises(exception.NoConsolePid, + console_utils._stop_console, + self.info['uuid']) + + mock_pid.assert_called_once_with(self.info['uuid']) + self.assertFalse(mock_execute.called) + mock_unlink.assert_called_once_with(pid_file) + + @mock.patch.object(utils, 'unlink_without_raise', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + @mock.patch.object(console_utils, '_get_console_pid', autospec=True) + def test__stop_console_nokill(self, mock_pid, mock_execute, mock_unlink): + pid_file = console_utils._get_console_pid_file(self.info['uuid']) + mock_pid.return_value = '12345' + mock_execute.side_effect = iter([processutils.ProcessExecutionError()]) + + self.assertRaises(processutils.ProcessExecutionError, + console_utils._stop_console, + self.info['uuid']) + + mock_pid.assert_called_once_with(self.info['uuid']) + mock_execute.assert_called_once_with('kill', mock_pid.return_value, + check_exit_code=[0, 99]) + mock_unlink.assert_called_once_with(pid_file) + + def _get_shellinabox_console(self, scheme): + generated_url = ( + console_utils.get_shellinabox_console_url(self.info['port'])) + console_host = CONF.my_ip + if netutils.is_valid_ipv6(console_host): + console_host = '[%s]' % console_host + http_url = "%s://%s:%s" % (scheme, console_host, self.info['port']) + self.assertEqual(http_url, generated_url) + + def test_get_shellinabox_console_url(self): + self._get_shellinabox_console('http') + + def test_get_shellinabox_console_https_url(self): + # specify terminal_cert_dir in /etc/ironic/ironic.conf + self.config(terminal_cert_dir='/tmp', group='console') + # use https + self._get_shellinabox_console('https') + + def test_make_persistent_password_file(self): + filepath = '%(tempdir)s/%(node_uuid)s' % { + 'tempdir': tempfile.gettempdir(), + 'node_uuid': self.info['uuid']} + password = ''.join([random.choice(string.ascii_letters) + for n in range(16)]) + console_utils.make_persistent_password_file(filepath, password) + # make sure file exists + self.assertTrue(os.path.exists(filepath)) + # make sure the content is correct + with open(filepath) as file: + content = file.read() + self.assertEqual(password, content) + # delete the file + os.unlink(filepath) + + @mock.patch.object(os, 'chmod', autospec=True) + def test_make_persistent_password_file_fail(self, mock_chmod): + mock_chmod.side_effect = IOError() + filepath = '%(tempdir)s/%(node_uuid)s' % { + 'tempdir': tempfile.gettempdir(), + 'node_uuid': self.info['uuid']} + self.assertRaises(exception.PasswordFileFailedToCreate, + console_utils.make_persistent_password_file, + filepath, + 'password') + + @mock.patch.object(subprocess, 'Popen', autospec=True) + @mock.patch.object(console_utils, '_ensure_console_pid_dir_exists', + autospec=True) + @mock.patch.object(console_utils, '_stop_console', autospec=True) + def test_start_shellinabox_console(self, mock_stop, mock_dir_exists, + mock_popen): + mock_popen.return_value.poll.return_value = 0 + + # touch the pid file + pid_file = console_utils._get_console_pid_file(self.info['uuid']) + open(pid_file, 'a').close() + self.addCleanup(os.remove, pid_file) + self.assertTrue(os.path.exists(pid_file)) + + console_utils.start_shellinabox_console(self.info['uuid'], + self.info['port'], + 'ls&') + + mock_stop.assert_called_once_with(self.info['uuid']) + mock_dir_exists.assert_called_once_with() + mock_popen.assert_called_once_with(mock.ANY, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + mock_popen.return_value.poll.assert_called_once_with() + + @mock.patch.object(subprocess, 'Popen', autospec=True) + @mock.patch.object(console_utils, '_ensure_console_pid_dir_exists', + autospec=True) + @mock.patch.object(console_utils, '_stop_console', autospec=True) + def test_start_shellinabox_console_nopid(self, mock_stop, mock_dir_exists, + mock_popen): + # no existing PID file before starting + mock_stop.side_effect = iter([exception.NoConsolePid('/tmp/blah')]) + mock_popen.return_value.poll.return_value = 0 + + # touch the pid file + pid_file = console_utils._get_console_pid_file(self.info['uuid']) + open(pid_file, 'a').close() + self.addCleanup(os.remove, pid_file) + self.assertTrue(os.path.exists(pid_file)) + + console_utils.start_shellinabox_console(self.info['uuid'], + self.info['port'], + 'ls&') + + mock_stop.assert_called_once_with(self.info['uuid']) + mock_dir_exists.assert_called_once_with() + mock_popen.assert_called_once_with(mock.ANY, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + mock_popen.return_value.poll.assert_called_once_with() + + @mock.patch.object(subprocess, 'Popen', autospec=True) + @mock.patch.object(console_utils, '_ensure_console_pid_dir_exists', + autospec=True) + @mock.patch.object(console_utils, '_stop_console', autospec=True) + def test_start_shellinabox_console_fail(self, mock_stop, mock_dir_exists, + mock_popen): + mock_popen.return_value.poll.return_value = 1 + mock_popen.return_value.communicate.return_value = ('output', 'error') + + self.assertRaises(exception.ConsoleSubprocessFailed, + console_utils.start_shellinabox_console, + self.info['uuid'], + self.info['port'], + 'ls&') + + mock_stop.assert_called_once_with(self.info['uuid']) + mock_dir_exists.assert_called_once_with() + mock_popen.assert_called_once_with(mock.ANY, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + mock_popen.return_value.poll.assert_called_once_with() + + @mock.patch.object(subprocess, 'Popen', autospec=True) + @mock.patch.object(console_utils, '_ensure_console_pid_dir_exists', + autospec=True) + @mock.patch.object(console_utils, '_stop_console', autospec=True) + def test_start_shellinabox_console_fail_nopiddir(self, mock_stop, + mock_dir_exists, + mock_popen): + mock_dir_exists.side_effect = iter( + [exception.ConsoleError(message='fail')]) + mock_popen.return_value.poll.return_value = 0 + + self.assertRaises(exception.ConsoleError, + console_utils.start_shellinabox_console, + self.info['uuid'], + self.info['port'], + 'ls&') + + mock_stop.assert_called_once_with(self.info['uuid']) + mock_dir_exists.assert_called_once_with() + self.assertFalse(mock_popen.called) + + @mock.patch.object(console_utils, '_stop_console', autospec=True) + def test_stop_shellinabox_console(self, mock_stop): + + console_utils.stop_shellinabox_console(self.info['uuid']) + + mock_stop.assert_called_once_with(self.info['uuid']) + + @mock.patch.object(console_utils, '_stop_console', autospec=True) + def test_stop_shellinabox_console_fail_nopid(self, mock_stop): + mock_stop.side_effect = iter([exception.NoConsolePid('/tmp/blah')]) + + console_utils.stop_shellinabox_console(self.info['uuid']) + + mock_stop.assert_called_once_with(self.info['uuid']) + + @mock.patch.object(console_utils, '_stop_console', autospec=True) + def test_stop_shellinabox_console_fail_nokill(self, mock_stop): + mock_stop.side_effect = iter([processutils.ProcessExecutionError()]) + + self.assertRaises(exception.ConsoleError, + console_utils.stop_shellinabox_console, + self.info['uuid']) + + mock_stop.assert_called_once_with(self.info['uuid']) diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py new file mode 100644 index 000000000..d43e7e716 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -0,0 +1,2288 @@ +# Copyright (c) 2012 NTT DOCOMO, INC. +# Copyright 2011 OpenStack Foundation +# Copyright 2011 Ilya Alekseyev +# +# 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 base64 +import gzip +import os +import shutil +import stat +import tempfile +import time +import types + +import mock +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_utils import uuidutils +import requests +import testtools + +from ironic.common import boot_devices +from ironic.common import disk_partitioner +from ironic.common import exception +from ironic.common import image_service +from ironic.common import images +from ironic.common import keystone +from ironic.common import states +from ironic.common import utils as common_utils +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import agent_client +from ironic.drivers.modules import deploy_utils as utils +from ironic.drivers.modules import image_cache +from ironic.drivers.modules import pxe +from ironic.tests.unit import base as tests_base +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INST_INFO_DICT = db_utils.get_test_pxe_instance_info() +DRV_INFO_DICT = db_utils.get_test_pxe_driver_info() +DRV_INTERNAL_INFO_DICT = db_utils.get_test_pxe_driver_internal_info() + +_PXECONF_DEPLOY = b""" +default deploy + +label deploy +kernel deploy_kernel +append initrd=deploy_ramdisk +ipappend 3 + +label boot_partition +kernel kernel +append initrd=ramdisk root={{ ROOT }} + +label boot_whole_disk +COM32 chain.c32 +append mbr:{{ DISK_IDENTIFIER }} + +label trusted_boot +kernel mboot +append tboot.gz --- kernel root={{ ROOT }} --- ramdisk +""" + +_PXECONF_BOOT_PARTITION = """ +default boot_partition + +label deploy +kernel deploy_kernel +append initrd=deploy_ramdisk +ipappend 3 + +label boot_partition +kernel kernel +append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef + +label boot_whole_disk +COM32 chain.c32 +append mbr:{{ DISK_IDENTIFIER }} + +label trusted_boot +kernel mboot +append tboot.gz --- kernel root=UUID=12345678-1234-1234-1234-1234567890abcdef \ +--- ramdisk +""" + +_PXECONF_BOOT_WHOLE_DISK = """ +default boot_whole_disk + +label deploy +kernel deploy_kernel +append initrd=deploy_ramdisk +ipappend 3 + +label boot_partition +kernel kernel +append initrd=ramdisk root={{ ROOT }} + +label boot_whole_disk +COM32 chain.c32 +append mbr:0x12345678 + +label trusted_boot +kernel mboot +append tboot.gz --- kernel root={{ ROOT }} --- ramdisk +""" + +_PXECONF_TRUSTED_BOOT = """ +default trusted_boot + +label deploy +kernel deploy_kernel +append initrd=deploy_ramdisk +ipappend 3 + +label boot_partition +kernel kernel +append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef + +label boot_whole_disk +COM32 chain.c32 +append mbr:{{ DISK_IDENTIFIER }} + +label trusted_boot +kernel mboot +append tboot.gz --- kernel root=UUID=12345678-1234-1234-1234-1234567890abcdef \ +--- ramdisk +""" + +_IPXECONF_DEPLOY = b""" +#!ipxe + +dhcp + +goto deploy + +:deploy +kernel deploy_kernel +initrd deploy_ramdisk +boot + +:boot_partition +kernel kernel +append initrd=ramdisk root={{ ROOT }} +boot + +:boot_whole_disk +kernel chain.c32 +append mbr:{{ DISK_IDENTIFIER }} +boot +""" + +_IPXECONF_BOOT_PARTITION = """ +#!ipxe + +dhcp + +goto boot_partition + +:deploy +kernel deploy_kernel +initrd deploy_ramdisk +boot + +:boot_partition +kernel kernel +append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef +boot + +:boot_whole_disk +kernel chain.c32 +append mbr:{{ DISK_IDENTIFIER }} +boot +""" + +_IPXECONF_BOOT_WHOLE_DISK = """ +#!ipxe + +dhcp + +goto boot_whole_disk + +:deploy +kernel deploy_kernel +initrd deploy_ramdisk +boot + +:boot_partition +kernel kernel +append initrd=ramdisk root={{ ROOT }} +boot + +:boot_whole_disk +kernel chain.c32 +append mbr:0x12345678 +boot +""" + +_UEFI_PXECONF_DEPLOY = b""" +default=deploy + +image=deploy_kernel + label=deploy + initrd=deploy_ramdisk + append="ro text" + +image=kernel + label=boot_partition + initrd=ramdisk + append="root={{ ROOT }}" + +image=chain.c32 + label=boot_whole_disk + append="mbr:{{ DISK_IDENTIFIER }}" +""" + +_UEFI_PXECONF_BOOT_PARTITION = """ +default=boot_partition + +image=deploy_kernel + label=deploy + initrd=deploy_ramdisk + append="ro text" + +image=kernel + label=boot_partition + initrd=ramdisk + append="root=UUID=12345678-1234-1234-1234-1234567890abcdef" + +image=chain.c32 + label=boot_whole_disk + append="mbr:{{ DISK_IDENTIFIER }}" +""" + +_UEFI_PXECONF_BOOT_WHOLE_DISK = """ +default=boot_whole_disk + +image=deploy_kernel + label=deploy + initrd=deploy_ramdisk + append="ro text" + +image=kernel + label=boot_partition + initrd=ramdisk + append="root={{ ROOT }}" + +image=chain.c32 + label=boot_whole_disk + append="mbr:0x12345678" +""" + +_UEFI_PXECONF_DEPLOY_GRUB = b""" +set default=deploy +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi deploy_kernel "ro text" + initrdefi deploy_ramdisk +} + +menuentry "boot_partition" { + linuxefi kernel "root=(( ROOT ))" + initrdefi ramdisk +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:(( DISK_IDENTIFIER )) +} +""" + +_UEFI_PXECONF_BOOT_PARTITION_GRUB = """ +set default=boot_partition +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi deploy_kernel "ro text" + initrdefi deploy_ramdisk +} + +menuentry "boot_partition" { + linuxefi kernel "root=UUID=12345678-1234-1234-1234-1234567890abcdef" + initrdefi ramdisk +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:(( DISK_IDENTIFIER )) +} +""" + +_UEFI_PXECONF_BOOT_WHOLE_DISK_GRUB = """ +set default=boot_whole_disk +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi deploy_kernel "ro text" + initrdefi deploy_ramdisk +} + +menuentry "boot_partition" { + linuxefi kernel "root=(( ROOT ))" + initrdefi ramdisk +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:0x12345678 +} +""" + + +@mock.patch.object(time, 'sleep', lambda seconds: None) +class PhysicalWorkTestCase(tests_base.TestCase): + + def _mock_calls(self, name_list): + patch_list = [mock.patch.object(utils, name, + spec_set=types.FunctionType) + for name in name_list] + mock_list = [patcher.start() for patcher in patch_list] + for patcher in patch_list: + self.addCleanup(patcher.stop) + + parent_mock = mock.MagicMock(spec=[]) + for mocker, name in zip(mock_list, name_list): + parent_mock.attach_mock(mocker, name) + return parent_mock + + @mock.patch.object(common_utils, 'mkfs', autospec=True) + def _test_deploy_partition_image(self, mock_mkfs, boot_option=None, + boot_mode=None): + """Check loosely all functions are called with right args.""" + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 64 + ephemeral_mb = 0 + ephemeral_format = None + configdrive_mb = 0 + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + dev = '/dev/fake' + swap_part = '/dev/fake-part1' + root_part = '/dev/fake-part2' + root_uuid = '12345678-1234-1234-12345678-12345678abcdef' + + name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi', 'make_partitions', + 'is_block_device', 'populate_image', 'block_uuid', + 'notify', 'destroy_disk_metadata'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.get_image_mb.return_value = 1 + parent_mock.is_block_device.return_value = True + parent_mock.block_uuid.return_value = root_uuid + parent_mock.make_partitions.return_value = {'root': root_part, + 'swap': swap_part} + + make_partitions_expected_args = [dev, root_mb, swap_mb, ephemeral_mb, + configdrive_mb, node_uuid] + make_partitions_expected_kwargs = {'commit': True} + deploy_kwargs = {} + + if boot_option: + make_partitions_expected_kwargs['boot_option'] = boot_option + deploy_kwargs['boot_option'] = boot_option + else: + make_partitions_expected_kwargs['boot_option'] = 'netboot' + + if boot_mode: + make_partitions_expected_kwargs['boot_mode'] = boot_mode + deploy_kwargs['boot_mode'] = boot_mode + else: + make_partitions_expected_kwargs['boot_mode'] = 'bios' + + # If no boot_option, then it should default to netboot. + calls_expected = [mock.call.get_image_mb(image_path), + mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.destroy_disk_metadata(dev, node_uuid), + mock.call.make_partitions( + *make_partitions_expected_args, + **make_partitions_expected_kwargs), + mock.call.is_block_device(root_part), + mock.call.is_block_device(swap_part), + mock.call.populate_image(image_path, root_part), + mock.call.block_uuid(root_part), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + uuids_dict_returned = utils.deploy_partition_image( + address, port, iqn, lun, image_path, root_mb, swap_mb, + ephemeral_mb, ephemeral_format, node_uuid, **deploy_kwargs) + + self.assertEqual(calls_expected, parent_mock.mock_calls) + expected_uuid_dict = { + 'root uuid': root_uuid, + 'efi system partition uuid': None} + self.assertEqual(expected_uuid_dict, uuids_dict_returned) + mock_mkfs.assert_called_once_with('swap', swap_part, 'swap1') + + def test_deploy_partition_image_without_boot_option(self): + self._test_deploy_partition_image() + + def test_deploy_partition_image_netboot(self): + self._test_deploy_partition_image(boot_option="netboot") + + def test_deploy_partition_image_localboot(self): + self._test_deploy_partition_image(boot_option="local") + + def test_deploy_partition_image_wo_boot_option_and_wo_boot_mode(self): + self._test_deploy_partition_image() + + def test_deploy_partition_image_netboot_bios(self): + self._test_deploy_partition_image(boot_option="netboot", + boot_mode="bios") + + def test_deploy_partition_image_localboot_bios(self): + self._test_deploy_partition_image(boot_option="local", + boot_mode="bios") + + def test_deploy_partition_image_netboot_uefi(self): + self._test_deploy_partition_image(boot_option="netboot", + boot_mode="uefi") + + @mock.patch.object(utils, 'get_image_mb', return_value=129, autospec=True) + def test_deploy_partition_image_image_exceeds_root_partition(self, + gim_mock): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 64 + ephemeral_mb = 0 + ephemeral_format = None + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + self.assertRaises(exception.InstanceDeployFailure, + utils.deploy_partition_image, address, port, iqn, + lun, image_path, root_mb, swap_mb, ephemeral_mb, + ephemeral_format, node_uuid) + + gim_mock.assert_called_once_with(image_path) + + # We mock utils.block_uuid separately here because we can't predict + # the order in which it will be called. + @mock.patch.object(utils, 'block_uuid', autospec=True) + @mock.patch.object(common_utils, 'mkfs', autospec=True) + def test_deploy_partition_image_localboot_uefi(self, mock_mkfs, + block_uuid_mock): + """Check loosely all functions are called with right args.""" + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 64 + ephemeral_mb = 0 + ephemeral_format = None + configdrive_mb = 0 + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + dev = '/dev/fake' + swap_part = '/dev/fake-part2' + root_part = '/dev/fake-part3' + efi_system_part = '/dev/fake-part1' + root_uuid = '12345678-1234-1234-12345678-12345678abcdef' + efi_system_part_uuid = '9036-482' + + name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi', 'make_partitions', + 'is_block_device', 'populate_image', 'notify', + 'destroy_disk_metadata'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.get_image_mb.return_value = 1 + parent_mock.is_block_device.return_value = True + + def block_uuid_side_effect(device): + if device == root_part: + return root_uuid + if device == efi_system_part: + return efi_system_part_uuid + + block_uuid_mock.side_effect = block_uuid_side_effect + parent_mock.make_partitions.return_value = { + 'root': root_part, 'swap': swap_part, + 'efi system partition': efi_system_part} + + # If no boot_option, then it should default to netboot. + calls_expected = [mock.call.get_image_mb(image_path), + mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.destroy_disk_metadata(dev, node_uuid), + mock.call.make_partitions(dev, root_mb, swap_mb, + ephemeral_mb, + configdrive_mb, + node_uuid, + commit=True, + boot_option="local", + boot_mode="uefi"), + mock.call.is_block_device(root_part), + mock.call.is_block_device(swap_part), + mock.call.is_block_device(efi_system_part), + mock.call.populate_image(image_path, root_part), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + uuid_dict_returned = utils.deploy_partition_image( + address, port, iqn, lun, image_path, root_mb, swap_mb, + ephemeral_mb, ephemeral_format, node_uuid, boot_option="local", + boot_mode="uefi") + + self.assertEqual(calls_expected, parent_mock.mock_calls) + block_uuid_mock.assert_any_call('/dev/fake-part1') + block_uuid_mock.assert_any_call('/dev/fake-part3') + expected_uuid_dict = { + 'root uuid': root_uuid, + 'efi system partition uuid': efi_system_part_uuid} + self.assertEqual(expected_uuid_dict, uuid_dict_returned) + expected_calls = [mock.call('vfat', efi_system_part, 'efi-part'), + mock.call('swap', swap_part, 'swap1')] + mock_mkfs.assert_has_calls(expected_calls) + + def test_deploy_partition_image_without_swap(self): + """Check loosely all functions are called with right args.""" + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 0 + ephemeral_mb = 0 + ephemeral_format = None + configdrive_mb = 0 + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + dev = '/dev/fake' + root_part = '/dev/fake-part1' + root_uuid = '12345678-1234-1234-12345678-12345678abcdef' + + name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi', 'make_partitions', + 'is_block_device', 'populate_image', 'block_uuid', + 'notify', 'destroy_disk_metadata'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.get_image_mb.return_value = 1 + parent_mock.is_block_device.return_value = True + parent_mock.block_uuid.return_value = root_uuid + parent_mock.make_partitions.return_value = {'root': root_part} + calls_expected = [mock.call.get_image_mb(image_path), + mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.destroy_disk_metadata(dev, node_uuid), + mock.call.make_partitions(dev, root_mb, swap_mb, + ephemeral_mb, + configdrive_mb, + node_uuid, + commit=True, + boot_option="netboot", + boot_mode="bios"), + mock.call.is_block_device(root_part), + mock.call.populate_image(image_path, root_part), + mock.call.block_uuid(root_part), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + uuid_dict_returned = utils.deploy_partition_image(address, port, iqn, + lun, image_path, + root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, + node_uuid) + + self.assertEqual(calls_expected, parent_mock.mock_calls) + self.assertEqual(root_uuid, uuid_dict_returned['root uuid']) + + @mock.patch.object(common_utils, 'mkfs', autospec=True) + def test_deploy_partition_image_with_ephemeral(self, mock_mkfs): + """Check loosely all functions are called with right args.""" + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 64 + ephemeral_mb = 256 + configdrive_mb = 0 + ephemeral_format = 'exttest' + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + dev = '/dev/fake' + ephemeral_part = '/dev/fake-part1' + swap_part = '/dev/fake-part2' + root_part = '/dev/fake-part3' + root_uuid = '12345678-1234-1234-12345678-12345678abcdef' + + name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi', 'make_partitions', + 'is_block_device', 'populate_image', 'block_uuid', + 'notify', 'destroy_disk_metadata'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.get_image_mb.return_value = 1 + parent_mock.is_block_device.return_value = True + parent_mock.block_uuid.return_value = root_uuid + parent_mock.make_partitions.return_value = { + 'swap': swap_part, 'ephemeral': ephemeral_part, 'root': root_part} + calls_expected = [mock.call.get_image_mb(image_path), + mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.destroy_disk_metadata(dev, node_uuid), + mock.call.make_partitions(dev, root_mb, swap_mb, + ephemeral_mb, + configdrive_mb, + node_uuid, + commit=True, + boot_option="netboot", + boot_mode="bios"), + mock.call.is_block_device(root_part), + mock.call.is_block_device(swap_part), + mock.call.is_block_device(ephemeral_part), + mock.call.populate_image(image_path, root_part), + mock.call.block_uuid(root_part), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + uuid_dict_returned = utils.deploy_partition_image(address, port, iqn, + lun, image_path, + root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, + node_uuid) + + self.assertEqual(calls_expected, parent_mock.mock_calls) + self.assertEqual(root_uuid, uuid_dict_returned['root uuid']) + expected_calls = [mock.call('swap', swap_part, 'swap1'), + mock.call(ephemeral_format, ephemeral_part, + 'ephemeral0')] + mock_mkfs.assert_has_calls(expected_calls) + + @mock.patch.object(common_utils, 'mkfs', autospec=True) + def test_deploy_partition_image_preserve_ephemeral(self, mock_mkfs): + """Check if all functions are called with right args.""" + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 64 + ephemeral_mb = 256 + ephemeral_format = 'exttest' + configdrive_mb = 0 + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + dev = '/dev/fake' + ephemeral_part = '/dev/fake-part1' + swap_part = '/dev/fake-part2' + root_part = '/dev/fake-part3' + root_uuid = '12345678-1234-1234-12345678-12345678abcdef' + + name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi', 'make_partitions', + 'is_block_device', 'populate_image', 'block_uuid', + 'notify', 'get_dev_block_size'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.get_image_mb.return_value = 1 + parent_mock.is_block_device.return_value = True + parent_mock.block_uuid.return_value = root_uuid + parent_mock.make_partitions.return_value = { + 'swap': swap_part, 'ephemeral': ephemeral_part, 'root': root_part} + parent_mock.block_uuid.return_value = root_uuid + calls_expected = [mock.call.get_image_mb(image_path), + mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.make_partitions(dev, root_mb, swap_mb, + ephemeral_mb, + configdrive_mb, + node_uuid, + commit=False, + boot_option="netboot", + boot_mode="bios"), + mock.call.is_block_device(root_part), + mock.call.is_block_device(swap_part), + mock.call.is_block_device(ephemeral_part), + mock.call.populate_image(image_path, root_part), + mock.call.block_uuid(root_part), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + uuid_dict_returned = utils.deploy_partition_image( + address, port, iqn, lun, image_path, root_mb, swap_mb, + ephemeral_mb, ephemeral_format, node_uuid, + preserve_ephemeral=True, boot_option="netboot") + self.assertEqual(calls_expected, parent_mock.mock_calls) + self.assertFalse(parent_mock.get_dev_block_size.called) + self.assertEqual(root_uuid, uuid_dict_returned['root uuid']) + mock_mkfs.assert_called_once_with('swap', swap_part, 'swap1') + + @mock.patch.object(common_utils, 'unlink_without_raise', autospec=True) + def test_deploy_partition_image_with_configdrive(self, mock_unlink): + """Check loosely all functions are called with right args.""" + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 0 + ephemeral_mb = 0 + configdrive_mb = 10 + ephemeral_format = None + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + configdrive_url = 'http://1.2.3.4/cd' + + dev = '/dev/fake' + configdrive_part = '/dev/fake-part1' + root_part = '/dev/fake-part2' + root_uuid = '12345678-1234-1234-12345678-12345678abcdef' + + name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi', 'make_partitions', + 'is_block_device', 'populate_image', 'block_uuid', + 'notify', 'destroy_disk_metadata', 'dd', + '_get_configdrive'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.get_image_mb.return_value = 1 + parent_mock.is_block_device.return_value = True + parent_mock.block_uuid.return_value = root_uuid + parent_mock.make_partitions.return_value = {'root': root_part, + 'configdrive': + configdrive_part} + parent_mock._get_configdrive.return_value = (10, 'configdrive-path') + calls_expected = [mock.call.get_image_mb(image_path), + mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.destroy_disk_metadata(dev, node_uuid), + mock.call._get_configdrive(configdrive_url, + node_uuid), + mock.call.make_partitions(dev, root_mb, swap_mb, + ephemeral_mb, + configdrive_mb, + node_uuid, + commit=True, + boot_option="netboot", + boot_mode="bios"), + mock.call.is_block_device(root_part), + mock.call.is_block_device(configdrive_part), + mock.call.dd(mock.ANY, configdrive_part), + mock.call.populate_image(image_path, root_part), + mock.call.block_uuid(root_part), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + uuid_dict_returned = utils.deploy_partition_image( + address, port, iqn, lun, image_path, root_mb, swap_mb, + ephemeral_mb, ephemeral_format, node_uuid, + configdrive=configdrive_url) + + self.assertEqual(calls_expected, parent_mock.mock_calls) + self.assertEqual(root_uuid, uuid_dict_returned['root uuid']) + mock_unlink.assert_called_once_with('configdrive-path') + + @mock.patch.object(utils, 'get_disk_identifier', autospec=True) + def test_deploy_whole_disk_image(self, mock_gdi): + """Check loosely all functions are called with right args.""" + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + dev = '/dev/fake' + name_list = ['get_dev', 'discovery', 'login_iscsi', 'logout_iscsi', + 'delete_iscsi', 'is_block_device', 'populate_image', + 'notify'] + parent_mock = self._mock_calls(name_list) + parent_mock.get_dev.return_value = dev + parent_mock.is_block_device.return_value = True + mock_gdi.return_value = '0x12345678' + calls_expected = [mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.is_block_device(dev), + mock.call.populate_image(image_path, dev), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + uuid_dict_returned = utils.deploy_disk_image(address, port, iqn, lun, + image_path, node_uuid) + + self.assertEqual(calls_expected, parent_mock.mock_calls) + self.assertEqual('0x12345678', uuid_dict_returned['disk identifier']) + + @mock.patch.object(common_utils, 'execute', autospec=True) + def test_verify_iscsi_connection_raises(self, mock_exec): + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.abc', ''] + self.assertRaises(exception.InstanceDeployFailure, + utils.verify_iscsi_connection, iqn) + self.assertEqual(3, mock_exec.call_count) + + @mock.patch.object(os.path, 'exists', autospec=True) + def test_check_file_system_for_iscsi_device_raises(self, mock_os): + iqn = 'iqn.xyz' + ip = "127.0.0.1" + port = "22" + mock_os.return_value = False + self.assertRaises(exception.InstanceDeployFailure, + utils.check_file_system_for_iscsi_device, + ip, port, iqn) + self.assertEqual(3, mock_os.call_count) + + @mock.patch.object(os.path, 'exists', autospec=True) + def test_check_file_system_for_iscsi_device(self, mock_os): + iqn = 'iqn.xyz' + ip = "127.0.0.1" + port = "22" + check_dir = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-1" % (ip, + port, + iqn) + + mock_os.return_value = True + utils.check_file_system_for_iscsi_device(ip, port, iqn) + mock_os.assert_called_once_with(check_dir) + + @mock.patch.object(common_utils, 'execute', autospec=True) + def test_verify_iscsi_connection(self, mock_exec): + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.xyz', ''] + utils.verify_iscsi_connection(iqn) + mock_exec.assert_called_once_with( + 'iscsiadm', + '-m', 'node', + '-S', + run_as_root=True, + check_exit_code=[0]) + + @mock.patch.object(common_utils, 'execute', autospec=True) + def test_force_iscsi_lun_update(self, mock_exec): + iqn = 'iqn.xyz' + utils.force_iscsi_lun_update(iqn) + mock_exec.assert_called_once_with( + 'iscsiadm', + '-m', 'node', + '-T', iqn, + '-R', + run_as_root=True, + check_exit_code=[0]) + + @mock.patch.object(common_utils, 'execute', autospec=True) + @mock.patch.object(utils, 'verify_iscsi_connection', autospec=True) + @mock.patch.object(utils, 'force_iscsi_lun_update', autospec=True) + @mock.patch.object(utils, 'check_file_system_for_iscsi_device', + autospec=True) + def test_login_iscsi_calls_verify_and_update(self, + mock_check_dev, + mock_update, + mock_verify, + mock_exec): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + mock_exec.return_value = ['iqn.xyz', ''] + utils.login_iscsi(address, port, iqn) + mock_exec.assert_called_once_with( + 'iscsiadm', + '-m', 'node', + '-p', '%s:%s' % (address, port), + '-T', iqn, + '--login', + run_as_root=True, + check_exit_code=[0], + attempts=5, + delay_on_retry=True) + + mock_verify.assert_called_once_with(iqn) + + mock_update.assert_called_once_with(iqn) + + mock_check_dev.assert_called_once_with(address, port, iqn) + + @mock.patch.object(utils, 'is_block_device', lambda d: True) + def test_always_logout_and_delete_iscsi(self): + """Check if logout_iscsi() and delete_iscsi() are called. + + Make sure that logout_iscsi() and delete_iscsi() are called once + login_iscsi() is invoked. + + """ + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + image_path = '/tmp/xyz/image' + root_mb = 128 + swap_mb = 64 + ephemeral_mb = 256 + ephemeral_format = 'exttest' + node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + dev = '/dev/fake' + + class TestException(Exception): + pass + + name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi', + 'logout_iscsi', 'delete_iscsi', 'work_on_disk'] + patch_list = [mock.patch.object(utils, name, + spec_set=types.FunctionType) + for name in name_list] + mock_list = [patcher.start() for patcher in patch_list] + for patcher in patch_list: + self.addCleanup(patcher.stop) + + parent_mock = mock.MagicMock(spec=[]) + for mocker, name in zip(mock_list, name_list): + parent_mock.attach_mock(mocker, name) + + parent_mock.get_dev.return_value = dev + parent_mock.get_image_mb.return_value = 1 + parent_mock.work_on_disk.side_effect = TestException + calls_expected = [mock.call.get_image_mb(image_path), + mock.call.get_dev(address, port, iqn, lun), + mock.call.discovery(address, port), + mock.call.login_iscsi(address, port, iqn), + mock.call.work_on_disk(dev, root_mb, swap_mb, + ephemeral_mb, + ephemeral_format, image_path, + node_uuid, configdrive=None, + preserve_ephemeral=False, + boot_option="netboot", + boot_mode="bios"), + mock.call.logout_iscsi(address, port, iqn), + mock.call.delete_iscsi(address, port, iqn)] + + self.assertRaises(TestException, utils.deploy_partition_image, + address, port, iqn, lun, image_path, + root_mb, swap_mb, ephemeral_mb, ephemeral_format, + node_uuid) + + self.assertEqual(calls_expected, parent_mock.mock_calls) + + +class SwitchPxeConfigTestCase(tests_base.TestCase): + + def _create_config(self, ipxe=False, boot_mode=None, boot_loader='elilo'): + (fd, fname) = tempfile.mkstemp() + if boot_mode == 'uefi': + if boot_loader == 'grub': + pxe_cfg = _UEFI_PXECONF_DEPLOY_GRUB + else: + pxe_cfg = _UEFI_PXECONF_DEPLOY + else: + pxe_cfg = _IPXECONF_DEPLOY if ipxe else _PXECONF_DEPLOY + os.write(fd, pxe_cfg) + os.close(fd) + self.addCleanup(os.unlink, fname) + return fname + + def test_switch_pxe_config_partition_image(self): + boot_mode = 'bios' + fname = self._create_config() + utils.switch_pxe_config(fname, + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_PXECONF_BOOT_PARTITION, pxeconf) + + def test_switch_pxe_config_whole_disk_image(self): + boot_mode = 'bios' + fname = self._create_config() + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_PXECONF_BOOT_WHOLE_DISK, pxeconf) + + def test_switch_pxe_config_trusted_boot(self): + boot_mode = 'bios' + fname = self._create_config() + utils.switch_pxe_config(fname, + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False, True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_PXECONF_TRUSTED_BOOT, pxeconf) + + def test_switch_ipxe_config_partition_image(self): + boot_mode = 'bios' + cfg.CONF.set_override('ipxe_enabled', True, 'pxe') + fname = self._create_config(ipxe=True) + utils.switch_pxe_config(fname, + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_IPXECONF_BOOT_PARTITION, pxeconf) + + def test_switch_ipxe_config_whole_disk_image(self): + boot_mode = 'bios' + cfg.CONF.set_override('ipxe_enabled', True, 'pxe') + fname = self._create_config(ipxe=True) + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_IPXECONF_BOOT_WHOLE_DISK, pxeconf) + + def test_switch_uefi_elilo_pxe_config_partition_image(self): + boot_mode = 'uefi' + fname = self._create_config(boot_mode=boot_mode) + utils.switch_pxe_config(fname, + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_UEFI_PXECONF_BOOT_PARTITION, pxeconf) + + def test_switch_uefi_elilo_config_whole_disk_image(self): + boot_mode = 'uefi' + fname = self._create_config(boot_mode=boot_mode) + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_UEFI_PXECONF_BOOT_WHOLE_DISK, pxeconf) + + def test_switch_uefi_grub_pxe_config_partition_image(self): + boot_mode = 'uefi' + fname = self._create_config(boot_mode=boot_mode, boot_loader='grub') + utils.switch_pxe_config(fname, + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_UEFI_PXECONF_BOOT_PARTITION_GRUB, pxeconf) + + def test_switch_uefi_grub_config_whole_disk_image(self): + boot_mode = 'uefi' + fname = self._create_config(boot_mode=boot_mode, boot_loader='grub') + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_UEFI_PXECONF_BOOT_WHOLE_DISK_GRUB, pxeconf) + + +@mock.patch('time.sleep', lambda sec: None) +class OtherFunctionTestCase(db_base.DbTestCase): + + def setUp(self): + super(OtherFunctionTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_pxe") + self.node = obj_utils.create_test_node(self.context, driver='fake_pxe') + + def test_get_dev(self): + expected = '/dev/disk/by-path/ip-1.2.3.4:5678-iscsi-iqn.fake-lun-9' + actual = utils.get_dev('1.2.3.4', 5678, 'iqn.fake', 9) + self.assertEqual(expected, actual) + + @mock.patch.object(os, 'stat', autospec=True) + @mock.patch.object(stat, 'S_ISBLK', autospec=True) + def test_is_block_device_works(self, mock_is_blk, mock_os): + device = '/dev/disk/by-path/ip-1.2.3.4:5678-iscsi-iqn.fake-lun-9' + mock_is_blk.return_value = True + mock_os().st_mode = 10000 + self.assertTrue(utils.is_block_device(device)) + mock_is_blk.assert_called_once_with(mock_os().st_mode) + + @mock.patch.object(os, 'stat', autospec=True) + def test_is_block_device_raises(self, mock_os): + device = '/dev/disk/by-path/ip-1.2.3.4:5678-iscsi-iqn.fake-lun-9' + mock_os.side_effect = OSError + self.assertRaises(exception.InstanceDeployFailure, + utils.is_block_device, device) + mock_os.assert_has_calls([mock.call(device)] * 3) + + @mock.patch.object(os.path, 'getsize', autospec=True) + @mock.patch.object(images, 'converted_size', autospec=True) + def test_get_image_mb(self, mock_csize, mock_getsize): + mb = 1024 * 1024 + + mock_getsize.return_value = 0 + mock_csize.return_value = 0 + self.assertEqual(0, utils.get_image_mb('x', False)) + self.assertEqual(0, utils.get_image_mb('x', True)) + mock_getsize.return_value = 1 + mock_csize.return_value = 1 + self.assertEqual(1, utils.get_image_mb('x', False)) + self.assertEqual(1, utils.get_image_mb('x', True)) + mock_getsize.return_value = mb + mock_csize.return_value = mb + self.assertEqual(1, utils.get_image_mb('x', False)) + self.assertEqual(1, utils.get_image_mb('x', True)) + mock_getsize.return_value = mb + 1 + mock_csize.return_value = mb + 1 + self.assertEqual(2, utils.get_image_mb('x', False)) + self.assertEqual(2, utils.get_image_mb('x', True)) + + def test_parse_root_device_hints(self): + self.node.properties['root_device'] = {'wwn': 123456} + expected = 'wwn=123456' + result = utils.parse_root_device_hints(self.node) + self.assertEqual(expected, result) + + def test_parse_root_device_hints_string_space(self): + self.node.properties['root_device'] = {'model': 'fake model'} + expected = 'model=fake%20model' + result = utils.parse_root_device_hints(self.node) + self.assertEqual(expected, result) + + def test_parse_root_device_hints_no_hints(self): + self.node.properties = {} + result = utils.parse_root_device_hints(self.node) + self.assertIsNone(result) + + def test_parse_root_device_hints_invalid_hints(self): + self.node.properties['root_device'] = {'vehicle': 'Owlship'} + self.assertRaises(exception.InvalidParameterValue, + utils.parse_root_device_hints, self.node) + + def test_parse_root_device_hints_invalid_size(self): + self.node.properties['root_device'] = {'size': 'not-int'} + self.assertRaises(exception.InvalidParameterValue, + utils.parse_root_device_hints, self.node) + + @mock.patch.object(utils, 'LOG', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(task_manager.TaskManager, 'process_event', + autospec=True) + def _test_set_failed_state(self, mock_event, mock_power, mock_log, + event_value=None, power_value=None, + log_calls=None): + err_msg = 'some failure' + mock_event.side_effect = event_value + mock_power.side_effect = power_value + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + utils.set_failed_state(task, err_msg) + mock_event.assert_called_once_with(task, 'fail') + mock_power.assert_called_once_with(task, states.POWER_OFF) + self.assertEqual(err_msg, task.node.last_error) + if log_calls: + mock_log.exception.assert_has_calls(log_calls) + else: + self.assertFalse(mock_log.called) + + def test_set_failed_state(self): + exc_state = exception.InvalidState('invalid state') + exc_param = exception.InvalidParameterValue('invalid parameter') + mock_call = mock.call(mock.ANY) + self._test_set_failed_state() + calls = [mock_call] + self._test_set_failed_state(event_value=iter([exc_state] * len(calls)), + log_calls=calls) + calls = [mock_call] + self._test_set_failed_state(power_value=iter([exc_param] * len(calls)), + log_calls=calls) + calls = [mock_call, mock_call] + self._test_set_failed_state(event_value=iter([exc_state] * len(calls)), + power_value=iter([exc_param] * len(calls)), + log_calls=calls) + + def test_get_boot_option(self): + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + result = utils.get_boot_option(self.node) + self.assertEqual("local", result) + + def test_get_boot_option_default_value(self): + self.node.instance_info = {} + result = utils.get_boot_option(self.node) + self.assertEqual("netboot", result) + + +@mock.patch.object(disk_partitioner.DiskPartitioner, 'commit', lambda _: None) +class WorkOnDiskTestCase(tests_base.TestCase): + + def setUp(self): + super(WorkOnDiskTestCase, self).setUp() + self.image_path = '/tmp/xyz/image' + self.root_mb = 128 + self.swap_mb = 64 + self.ephemeral_mb = 0 + self.ephemeral_format = None + self.configdrive_mb = 0 + self.dev = '/dev/fake' + self.swap_part = '/dev/fake-part1' + self.root_part = '/dev/fake-part2' + + self.mock_ibd_obj = mock.patch.object( + utils, 'is_block_device', autospec=True) + self.mock_ibd = self.mock_ibd_obj.start() + self.addCleanup(self.mock_ibd_obj.stop) + self.mock_mp_obj = mock.patch.object( + utils, 'make_partitions', autospec=True) + self.mock_mp = self.mock_mp_obj.start() + self.addCleanup(self.mock_mp_obj.stop) + self.mock_remlbl_obj = mock.patch.object( + utils, 'destroy_disk_metadata', autospec=True) + self.mock_remlbl = self.mock_remlbl_obj.start() + self.addCleanup(self.mock_remlbl_obj.stop) + self.mock_mp.return_value = {'swap': self.swap_part, + 'root': self.root_part} + + def test_no_root_partition(self): + self.mock_ibd.return_value = False + self.assertRaises(exception.InstanceDeployFailure, + utils.work_on_disk, self.dev, self.root_mb, + self.swap_mb, self.ephemeral_mb, + self.ephemeral_format, self.image_path, 'fake-uuid') + self.mock_ibd.assert_called_once_with(self.root_part) + self.mock_mp.assert_called_once_with(self.dev, self.root_mb, + self.swap_mb, self.ephemeral_mb, + self.configdrive_mb, + 'fake-uuid', + commit=True, + boot_option="netboot", + boot_mode="bios") + + def test_no_swap_partition(self): + self.mock_ibd.side_effect = iter([True, False]) + calls = [mock.call(self.root_part), + mock.call(self.swap_part)] + self.assertRaises(exception.InstanceDeployFailure, + utils.work_on_disk, self.dev, self.root_mb, + self.swap_mb, self.ephemeral_mb, + self.ephemeral_format, self.image_path, 'fake-uuid') + self.assertEqual(self.mock_ibd.call_args_list, calls) + self.mock_mp.assert_called_once_with(self.dev, self.root_mb, + self.swap_mb, self.ephemeral_mb, + self.configdrive_mb, + 'fake-uuid', + commit=True, + boot_option="netboot", + boot_mode="bios") + + def test_no_ephemeral_partition(self): + ephemeral_part = '/dev/fake-part1' + swap_part = '/dev/fake-part2' + root_part = '/dev/fake-part3' + ephemeral_mb = 256 + ephemeral_format = 'exttest' + + self.mock_mp.return_value = {'ephemeral': ephemeral_part, + 'swap': swap_part, + 'root': root_part} + self.mock_ibd.side_effect = iter([True, True, False]) + calls = [mock.call(root_part), + mock.call(swap_part), + mock.call(ephemeral_part)] + self.assertRaises(exception.InstanceDeployFailure, + utils.work_on_disk, self.dev, self.root_mb, + self.swap_mb, ephemeral_mb, ephemeral_format, + self.image_path, 'fake-uuid') + self.assertEqual(self.mock_ibd.call_args_list, calls) + self.mock_mp.assert_called_once_with(self.dev, self.root_mb, + self.swap_mb, ephemeral_mb, + self.configdrive_mb, + 'fake-uuid', + commit=True, + boot_option="netboot", + boot_mode="bios") + + @mock.patch.object(common_utils, 'unlink_without_raise', autospec=True) + @mock.patch.object(utils, '_get_configdrive', autospec=True) + def test_no_configdrive_partition(self, mock_configdrive, mock_unlink): + mock_configdrive.return_value = (10, 'fake-path') + swap_part = '/dev/fake-part1' + configdrive_part = '/dev/fake-part2' + root_part = '/dev/fake-part3' + configdrive_url = 'http://1.2.3.4/cd' + configdrive_mb = 10 + + self.mock_mp.return_value = {'swap': swap_part, + 'configdrive': configdrive_part, + 'root': root_part} + self.mock_ibd.side_effect = iter([True, True, False]) + calls = [mock.call(root_part), + mock.call(swap_part), + mock.call(configdrive_part)] + self.assertRaises(exception.InstanceDeployFailure, + utils.work_on_disk, self.dev, self.root_mb, + self.swap_mb, self.ephemeral_mb, + self.ephemeral_format, self.image_path, 'fake-uuid', + preserve_ephemeral=False, + configdrive=configdrive_url, + boot_option="netboot") + self.assertEqual(self.mock_ibd.call_args_list, calls) + self.mock_mp.assert_called_once_with(self.dev, self.root_mb, + self.swap_mb, self.ephemeral_mb, + configdrive_mb, + 'fake-uuid', + commit=True, + boot_option="netboot", + boot_mode="bios") + mock_unlink.assert_called_once_with('fake-path') + + +@mock.patch.object(common_utils, 'execute', autospec=True) +class MakePartitionsTestCase(tests_base.TestCase): + + def setUp(self): + super(MakePartitionsTestCase, self).setUp() + self.dev = 'fake-dev' + self.root_mb = 1024 + self.swap_mb = 512 + self.ephemeral_mb = 0 + self.configdrive_mb = 0 + self.parted_static_cmd = ['parted', '-a', 'optimal', '-s', self.dev, + '--', 'unit', 'MiB', 'mklabel', 'msdos'] + + def _test_make_partitions(self, mock_exc, boot_option): + mock_exc.return_value = (None, None) + utils.make_partitions(self.dev, self.root_mb, self.swap_mb, + self.ephemeral_mb, self.configdrive_mb, + '12345678-1234-1234-1234-1234567890abcxyz', + boot_option=boot_option) + + expected_mkpart = ['mkpart', 'primary', 'linux-swap', '1', '513', + 'mkpart', 'primary', '', '513', '1537'] + if boot_option == "local": + expected_mkpart.extend(['set', '2', 'boot', 'on']) + parted_cmd = self.parted_static_cmd + expected_mkpart + parted_call = mock.call(*parted_cmd, run_as_root=True, + check_exit_code=[0]) + fuser_cmd = ['fuser', 'fake-dev'] + fuser_call = mock.call(*fuser_cmd, run_as_root=True, + check_exit_code=[0, 1]) + mock_exc.assert_has_calls([parted_call, fuser_call]) + + def test_make_partitions(self, mock_exc): + self._test_make_partitions(mock_exc, boot_option="netboot") + + def test_make_partitions_local_boot(self, mock_exc): + self._test_make_partitions(mock_exc, boot_option="local") + + def test_make_partitions_with_ephemeral(self, mock_exc): + self.ephemeral_mb = 2048 + expected_mkpart = ['mkpart', 'primary', '', '1', '2049', + 'mkpart', 'primary', 'linux-swap', '2049', '2561', + 'mkpart', 'primary', '', '2561', '3585'] + cmd = self.parted_static_cmd + expected_mkpart + mock_exc.return_value = (None, None) + utils.make_partitions(self.dev, self.root_mb, self.swap_mb, + self.ephemeral_mb, self.configdrive_mb, + '12345678-1234-1234-1234-1234567890abcxyz') + + parted_call = mock.call(*cmd, run_as_root=True, check_exit_code=[0]) + mock_exc.assert_has_calls([parted_call]) + + +@mock.patch.object(utils, 'get_dev_block_size', autospec=True) +@mock.patch.object(common_utils, 'execute', autospec=True) +class DestroyMetaDataTestCase(tests_base.TestCase): + + def setUp(self): + super(DestroyMetaDataTestCase, self).setUp() + self.dev = 'fake-dev' + self.node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + def test_destroy_disk_metadata(self, mock_exec, mock_gz): + mock_gz.return_value = 64 + expected_calls = [mock.call('dd', 'if=/dev/zero', 'of=fake-dev', + 'bs=512', 'count=36', run_as_root=True, + check_exit_code=[0], + use_standard_locale=True), + mock.call('dd', 'if=/dev/zero', 'of=fake-dev', + 'bs=512', 'count=36', 'seek=28', + run_as_root=True, + check_exit_code=[0], + use_standard_locale=True)] + utils.destroy_disk_metadata(self.dev, self.node_uuid) + mock_exec.assert_has_calls(expected_calls) + self.assertTrue(mock_gz.called) + + def test_destroy_disk_metadata_get_dev_size_fail(self, mock_exec, mock_gz): + mock_gz.side_effect = processutils.ProcessExecutionError + + expected_call = [mock.call('dd', 'if=/dev/zero', 'of=fake-dev', + 'bs=512', 'count=36', run_as_root=True, + check_exit_code=[0], + use_standard_locale=True)] + self.assertRaises(processutils.ProcessExecutionError, + utils.destroy_disk_metadata, + self.dev, + self.node_uuid) + mock_exec.assert_has_calls(expected_call) + + def test_destroy_disk_metadata_dd_fail(self, mock_exec, mock_gz): + mock_exec.side_effect = processutils.ProcessExecutionError + + expected_call = [mock.call('dd', 'if=/dev/zero', 'of=fake-dev', + 'bs=512', 'count=36', run_as_root=True, + check_exit_code=[0], + use_standard_locale=True)] + self.assertRaises(processutils.ProcessExecutionError, + utils.destroy_disk_metadata, + self.dev, + self.node_uuid) + mock_exec.assert_has_calls(expected_call) + self.assertFalse(mock_gz.called) + + +@mock.patch.object(common_utils, 'execute', autospec=True) +class GetDeviceBlockSizeTestCase(tests_base.TestCase): + + def setUp(self): + super(GetDeviceBlockSizeTestCase, self).setUp() + self.dev = 'fake-dev' + self.node_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + + def test_get_dev_block_size(self, mock_exec): + mock_exec.return_value = ("64", "") + expected_call = [mock.call('blockdev', '--getsz', self.dev, + run_as_root=True, check_exit_code=[0])] + utils.get_dev_block_size(self.dev) + mock_exec.assert_has_calls(expected_call) + + +@mock.patch.object(utils, 'dd', autospec=True) +@mock.patch.object(images, 'qemu_img_info', autospec=True) +@mock.patch.object(images, 'convert_image', autospec=True) +class PopulateImageTestCase(tests_base.TestCase): + + def setUp(self): + super(PopulateImageTestCase, self).setUp() + + def test_populate_raw_image(self, mock_cg, mock_qinfo, mock_dd): + type(mock_qinfo.return_value).file_format = mock.PropertyMock( + return_value='raw') + utils.populate_image('src', 'dst') + mock_dd.assert_called_once_with('src', 'dst') + self.assertFalse(mock_cg.called) + + def test_populate_qcow2_image(self, mock_cg, mock_qinfo, mock_dd): + type(mock_qinfo.return_value).file_format = mock.PropertyMock( + return_value='qcow2') + utils.populate_image('src', 'dst') + mock_cg.assert_called_once_with('src', 'dst', 'raw', True) + self.assertFalse(mock_dd.called) + + +@mock.patch.object(utils, 'is_block_device', lambda d: True) +@mock.patch.object(utils, 'block_uuid', lambda p: 'uuid') +@mock.patch.object(utils, 'dd', lambda *_: None) +@mock.patch.object(images, 'convert_image', lambda *_: None) +@mock.patch.object(common_utils, 'mkfs', lambda *_: None) +# NOTE(dtantsur): destroy_disk_metadata resets file size, disabling it +@mock.patch.object(utils, 'destroy_disk_metadata', lambda *_: None) +class RealFilePartitioningTestCase(tests_base.TestCase): + """This test applies some real-world partitioning scenario to a file. + + This test covers the whole partitioning, mocking everything not possible + on a file. That helps us assure, that we do all partitioning math properly + and also conducts integration testing of DiskPartitioner. + """ + + def setUp(self): + super(RealFilePartitioningTestCase, self).setUp() + # NOTE(dtantsur): no parted utility on gate-ironic-python26 + try: + common_utils.execute('parted', '--version') + except OSError as exc: + self.skipTest('parted utility was not found: %s' % exc) + self.file = tempfile.NamedTemporaryFile(delete=False) + # NOTE(ifarkas): the file needs to be closed, so fuser won't report + # any usage + self.file.close() + # NOTE(dtantsur): 20 MiB file with zeros + common_utils.execute('dd', 'if=/dev/zero', 'of=%s' % self.file.name, + 'bs=1', 'count=0', 'seek=20MiB') + + @staticmethod + def _run_without_root(func, *args, **kwargs): + """Make sure root is not required when using utils.execute.""" + real_execute = common_utils.execute + + def fake_execute(*cmd, **kwargs): + kwargs['run_as_root'] = False + return real_execute(*cmd, **kwargs) + + with mock.patch.object(common_utils, 'execute', fake_execute): + return func(*args, **kwargs) + + def test_different_sizes(self): + # NOTE(dtantsur): Keep this list in order with expected partitioning + fields = ['ephemeral_mb', 'swap_mb', 'root_mb'] + variants = ((0, 0, 12), (4, 2, 8), (0, 4, 10), (5, 0, 10)) + for variant in variants: + kwargs = dict(zip(fields, variant)) + self._run_without_root(utils.work_on_disk, self.file.name, + ephemeral_format='ext4', node_uuid='', + image_path='path', **kwargs) + part_table = self._run_without_root( + disk_partitioner.list_partitions, self.file.name) + for part, expected_size in zip(part_table, filter(None, variant)): + self.assertEqual(expected_size, part['size'], + "comparison failed for %s" % list(variant)) + + def test_whole_disk(self): + # 6 MiB ephemeral + 3 MiB swap + 9 MiB root + 1 MiB for MBR + # + 1 MiB MAGIC == 20 MiB whole disk + # TODO(dtantsur): figure out why we need 'magic' 1 more MiB + # and why the is different on Ubuntu and Fedora (see below) + self._run_without_root(utils.work_on_disk, self.file.name, + root_mb=9, ephemeral_mb=6, swap_mb=3, + ephemeral_format='ext4', node_uuid='', + image_path='path') + part_table = self._run_without_root( + disk_partitioner.list_partitions, self.file.name) + sizes = [part['size'] for part in part_table] + # NOTE(dtantsur): parted in Ubuntu 12.04 will occupy the last MiB, + # parted in Fedora 20 won't - thus two possible variants for last part + self.assertEqual([6, 3], sizes[:2], + "unexpected partitioning %s" % part_table) + self.assertIn(sizes[2], (9, 10)) + + @mock.patch.object(image_cache, 'clean_up_caches', autospec=True) + def test_fetch_images(self, mock_clean_up_caches): + + mock_cache = mock.MagicMock( + spec_set=['fetch_image', 'master_dir'], master_dir='master_dir') + utils.fetch_images(None, mock_cache, [('uuid', 'path')]) + mock_clean_up_caches.assert_called_once_with(None, 'master_dir', + [('uuid', 'path')]) + mock_cache.fetch_image.assert_called_once_with('uuid', 'path', + ctx=None, + force_raw=True) + + @mock.patch.object(image_cache, 'clean_up_caches', autospec=True) + def test_fetch_images_fail(self, mock_clean_up_caches): + + exc = exception.InsufficientDiskSpace(path='a', + required=2, + actual=1) + + mock_cache = mock.MagicMock( + spec_set=['master_dir'], master_dir='master_dir') + mock_clean_up_caches.side_effect = iter([exc]) + self.assertRaises(exception.InstanceDeployFailure, + utils.fetch_images, + None, + mock_cache, + [('uuid', 'path')]) + mock_clean_up_caches.assert_called_once_with(None, 'master_dir', + [('uuid', 'path')]) + + +@mock.patch.object(tempfile, 'NamedTemporaryFile', autospec=True) +@mock.patch.object(shutil, 'copyfileobj', autospec=True) +@mock.patch.object(requests, 'get', autospec=True) +class GetConfigdriveTestCase(tests_base.TestCase): + + def setUp(self): + super(GetConfigdriveTestCase, self).setUp() + # NOTE(lucasagomes): "name" can't be passed to Mock() when + # instantiating the object because it's an expected parameter. + # https://docs.python.org/3/library/unittest.mock.html + self.fake_configdrive_file = mock.Mock(tell=lambda *_: 123) + self.fake_configdrive_file.name = '/tmp/foo' + + @mock.patch.object(gzip, 'GzipFile', autospec=True) + def test_get_configdrive(self, mock_gzip, mock_requests, mock_copy, + mock_file): + mock_file.return_value = self.fake_configdrive_file + mock_requests.return_value = mock.MagicMock( + spec_set=['content'], content='Zm9vYmFy') + utils._get_configdrive('http://1.2.3.4/cd', 'fake-node-uuid') + mock_requests.assert_called_once_with('http://1.2.3.4/cd') + mock_gzip.assert_called_once_with('configdrive', 'rb', + fileobj=mock.ANY) + mock_copy.assert_called_once_with(mock.ANY, mock.ANY) + mock_file.assert_called_once_with(prefix='configdrive', + dir=cfg.CONF.tempdir, delete=False) + + @mock.patch.object(gzip, 'GzipFile', autospec=True) + def test_get_configdrive_base64_string(self, mock_gzip, mock_requests, + mock_copy, mock_file): + mock_file.return_value = self.fake_configdrive_file + utils._get_configdrive('Zm9vYmFy', 'fake-node-uuid') + self.assertFalse(mock_requests.called) + mock_gzip.assert_called_once_with('configdrive', 'rb', + fileobj=mock.ANY) + mock_copy.assert_called_once_with(mock.ANY, mock.ANY) + mock_file.assert_called_once_with(prefix='configdrive', + dir=cfg.CONF.tempdir, delete=False) + + def test_get_configdrive_bad_url(self, mock_requests, mock_copy, + mock_file): + mock_requests.side_effect = requests.exceptions.RequestException + self.assertRaises(exception.InstanceDeployFailure, + utils._get_configdrive, 'http://1.2.3.4/cd', + 'fake-node-uuid') + self.assertFalse(mock_copy.called) + self.assertFalse(mock_file.called) + + @mock.patch.object(base64, 'b64decode', autospec=True) + def test_get_configdrive_base64_error(self, mock_b64, mock_requests, + mock_copy, mock_file): + mock_b64.side_effect = TypeError + self.assertRaises(exception.InstanceDeployFailure, + utils._get_configdrive, + 'malformed', 'fake-node-uuid') + mock_b64.assert_called_once_with('malformed') + self.assertFalse(mock_copy.called) + self.assertFalse(mock_file.called) + + @mock.patch.object(gzip, 'GzipFile', autospec=True) + def test_get_configdrive_gzip_error(self, mock_gzip, mock_requests, + mock_copy, mock_file): + mock_file.return_value = self.fake_configdrive_file + mock_requests.return_value = mock.MagicMock( + spec_set=['content'], content='Zm9vYmFy') + mock_copy.side_effect = IOError + self.assertRaises(exception.InstanceDeployFailure, + utils._get_configdrive, 'http://1.2.3.4/cd', + 'fake-node-uuid') + mock_requests.assert_called_once_with('http://1.2.3.4/cd') + mock_gzip.assert_called_once_with('configdrive', 'rb', + fileobj=mock.ANY) + mock_copy.assert_called_once_with(mock.ANY, mock.ANY) + mock_file.assert_called_once_with(prefix='configdrive', + dir=cfg.CONF.tempdir, delete=False) + + +class VirtualMediaDeployUtilsTestCase(db_base.DbTestCase): + + def setUp(self): + super(VirtualMediaDeployUtilsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="iscsi_ilo") + info_dict = db_utils.get_test_ilo_info() + self.node = obj_utils.create_test_node( + self.context, driver='iscsi_ilo', driver_info=info_dict) + + def test_get_single_nic_with_vif_port_id(self): + obj_utils.create_test_port( + self.context, node_id=self.node.id, address='aa:bb:cc:dd:ee:ff', + uuid=uuidutils.generate_uuid(), + extra={'vif_port_id': 'test-vif-A'}, driver='iscsi_ilo') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + address = utils.get_single_nic_with_vif_port_id(task) + self.assertEqual('aa:bb:cc:dd:ee:ff', address) + + +class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase): + + def setUp(self): + super(ParseInstanceInfoCapabilitiesTestCase, self).setUp() + self.node = obj_utils.get_test_node(self.context, driver='fake') + + def test_parse_instance_info_capabilities_string(self): + self.node.instance_info = {'capabilities': '{"cat": "meow"}'} + expected_result = {"cat": "meow"} + result = utils.parse_instance_info_capabilities(self.node) + self.assertEqual(expected_result, result) + + def test_parse_instance_info_capabilities(self): + self.node.instance_info = {'capabilities': {"dog": "wuff"}} + expected_result = {"dog": "wuff"} + result = utils.parse_instance_info_capabilities(self.node) + self.assertEqual(expected_result, result) + + def test_parse_instance_info_invalid_type(self): + self.node.instance_info = {'capabilities': 'not-a-dict'} + self.assertRaises(exception.InvalidParameterValue, + utils.parse_instance_info_capabilities, self.node) + + def test_is_secure_boot_requested_true(self): + self.node.instance_info = {'capabilities': {"secure_boot": "tRue"}} + self.assertTrue(utils.is_secure_boot_requested(self.node)) + + def test_is_secure_boot_requested_false(self): + self.node.instance_info = {'capabilities': {"secure_boot": "false"}} + self.assertFalse(utils.is_secure_boot_requested(self.node)) + + def test_is_secure_boot_requested_invalid(self): + self.node.instance_info = {'capabilities': {"secure_boot": "invalid"}} + self.assertFalse(utils.is_secure_boot_requested(self.node)) + + def test_is_trusted_boot_requested_true(self): + self.node.instance_info = {'capabilities': {"trusted_boot": "true"}} + self.assertTrue(utils.is_trusted_boot_requested(self.node)) + + def test_is_trusted_boot_requested_false(self): + self.node.instance_info = {'capabilities': {"trusted_boot": "false"}} + self.assertFalse(utils.is_trusted_boot_requested(self.node)) + + def test_is_trusted_boot_requested_invalid(self): + self.node.instance_info = {'capabilities': {"trusted_boot": "invalid"}} + self.assertFalse(utils.is_trusted_boot_requested(self.node)) + + def test_get_boot_mode_for_deploy_using_capabilities(self): + properties = {'capabilities': 'boot_mode:uefi,cap2:value2'} + self.node.properties = properties + + result = utils.get_boot_mode_for_deploy(self.node) + self.assertEqual('uefi', result) + + def test_get_boot_mode_for_deploy_using_instance_info_cap(self): + instance_info = {'capabilities': {'secure_boot': 'True'}} + self.node.instance_info = instance_info + + result = utils.get_boot_mode_for_deploy(self.node) + self.assertEqual('uefi', result) + + instance_info = {'capabilities': {'trusted_boot': 'True'}} + self.node.instance_info = instance_info + + result = utils.get_boot_mode_for_deploy(self.node) + self.assertEqual('bios', result) + + instance_info = {'capabilities': {'trusted_boot': 'True'}, + 'capabilities': {'secure_boot': 'True'}} + self.node.instance_info = instance_info + + result = utils.get_boot_mode_for_deploy(self.node) + self.assertEqual('uefi', result) + + def test_get_boot_mode_for_deploy_using_instance_info(self): + instance_info = {'deploy_boot_mode': 'bios'} + self.node.instance_info = instance_info + + result = utils.get_boot_mode_for_deploy(self.node) + self.assertEqual('bios', result) + + def test_validate_boot_mode_capability(self): + prop = {'capabilities': 'boot_mode:uefi,cap2:value2'} + self.node.properties = prop + + result = utils.validate_capabilities(self.node) + self.assertIsNone(result) + + def test_validate_boot_mode_capability_with_exc(self): + prop = {'capabilities': 'boot_mode:UEFI,cap2:value2'} + self.node.properties = prop + + self.assertRaises(exception.InvalidParameterValue, + utils.validate_capabilities, self.node) + + def test_validate_boot_mode_capability_instance_info(self): + inst_info = {'capabilities': {"boot_mode": "uefi", "cap2": "value2"}} + self.node.instance_info = inst_info + + result = utils.validate_capabilities(self.node) + self.assertIsNone(result) + + def test_validate_boot_mode_capability_instance_info_with_exc(self): + inst_info = {'capabilities': {"boot_mode": "UEFI", "cap2": "value2"}} + self.node.instance_info = inst_info + + self.assertRaises(exception.InvalidParameterValue, + utils.validate_capabilities, self.node) + + def test_validate_trusted_boot_capability(self): + properties = {'capabilities': 'trusted_boot:value'} + self.node.properties = properties + self.assertRaises(exception.InvalidParameterValue, + utils.validate_capabilities, self.node) + + def test_all_supported_capabilities(self): + self.assertEqual(('local', 'netboot'), + utils.SUPPORTED_CAPABILITIES['boot_option']) + self.assertEqual(('bios', 'uefi'), + utils.SUPPORTED_CAPABILITIES['boot_mode']) + self.assertEqual(('true', 'false'), + utils.SUPPORTED_CAPABILITIES['secure_boot']) + self.assertEqual(('true', 'false'), + utils.SUPPORTED_CAPABILITIES['trusted_boot']) + + +class TrySetBootDeviceTestCase(db_base.DbTestCase): + + def setUp(self): + super(TrySetBootDeviceTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake") + self.node = obj_utils.create_test_node(self.context, driver="fake") + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + def test_try_set_boot_device_okay(self, node_set_boot_device_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + utils.try_set_boot_device(task, boot_devices.DISK, + persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + + @mock.patch.object(utils, 'LOG', autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + def test_try_set_boot_device_ipmifailure_uefi( + self, node_set_boot_device_mock, log_mock): + self.node.properties = {'capabilities': 'boot_mode:uefi'} + self.node.save() + node_set_boot_device_mock.side_effect = iter( + [exception.IPMIFailure(cmd='a')]) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + utils.try_set_boot_device(task, boot_devices.DISK, + persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + log_mock.warning.assert_called_once_with(mock.ANY) + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + def test_try_set_boot_device_ipmifailure_bios( + self, node_set_boot_device_mock): + node_set_boot_device_mock.side_effect = iter( + [exception.IPMIFailure(cmd='a')]) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IPMIFailure, + utils.try_set_boot_device, + task, boot_devices.DISK, persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + def test_try_set_boot_device_some_other_exception( + self, node_set_boot_device_mock): + exc = exception.IloOperationError(operation="qwe", error="error") + node_set_boot_device_mock.side_effect = iter([exc]) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + utils.try_set_boot_device, + task, boot_devices.DISK, persistent=True) + node_set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + + +class AgentMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(AgentMethodsTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_agent') + n = {'driver': 'fake_agent', + 'driver_internal_info': {'agent_url': 'http://127.0.0.1:9999'}} + + self.node = obj_utils.create_test_node(self.context, **n) + self.ports = [obj_utils.create_test_port(self.context, + node_id=self.node.id)] + + self.clean_steps = { + 'hardware_manager_version': '1', + 'clean_steps': { + 'GenericHardwareManager': [ + {'interface': 'deploy', + 'step': 'erase_devices', + 'priority': 20}, + ], + 'SpecificHardwareManager': [ + {'interface': 'deploy', + 'step': 'update_firmware', + 'priority': 30}, + {'interface': 'raid', + 'step': 'create_configuration', + 'priority': 10}, + ] + } + } + + @mock.patch('ironic.objects.Port.list_by_node_id', + spec_set=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'get_clean_steps', + autospec=True) + def test_get_clean_steps(self, client_mock, list_ports_mock): + client_mock.return_value = { + 'command_result': self.clean_steps} + list_ports_mock.return_value = self.ports + + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + response = utils.agent_get_clean_steps(task) + client_mock.assert_called_once_with(mock.ANY, task.node, + self.ports) + self.assertEqual('1', task.node.driver_internal_info[ + 'hardware_manager_version']) + + # Since steps are returned in dicts, they have non-deterministic + # ordering + self.assertEqual(2, len(response)) + self.assertIn(self.clean_steps['clean_steps'][ + 'GenericHardwareManager'][0], response) + self.assertIn(self.clean_steps['clean_steps'][ + 'SpecificHardwareManager'][0], response) + + @mock.patch('ironic.objects.Port.list_by_node_id', + spec_set=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'get_clean_steps', + autospec=True) + def test_get_clean_steps_missing_steps(self, client_mock, + list_ports_mock): + del self.clean_steps['clean_steps'] + client_mock.return_value = { + 'command_result': self.clean_steps} + list_ports_mock.return_value = self.ports + + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + self.assertRaises(exception.NodeCleaningFailure, + utils.agent_get_clean_steps, + task) + client_mock.assert_called_once_with(mock.ANY, task.node, + self.ports) + + @mock.patch('ironic.objects.Port.list_by_node_id', + spec_set=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'execute_clean_step', + autospec=True) + def test_execute_clean_step(self, client_mock, list_ports_mock): + client_mock.return_value = { + 'command_status': 'SUCCEEDED'} + list_ports_mock.return_value = self.ports + + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + response = utils.agent_execute_clean_step( + task, + self.clean_steps['clean_steps']['GenericHardwareManager'][0]) + self.assertEqual(states.CLEANWAIT, response) + + @mock.patch('ironic.objects.Port.list_by_node_id', + spec_set=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'execute_clean_step', + autospec=True) + def test_execute_clean_step_running(self, client_mock, list_ports_mock): + client_mock.return_value = { + 'command_status': 'RUNNING'} + list_ports_mock.return_value = self.ports + + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + response = utils.agent_execute_clean_step( + task, + self.clean_steps['clean_steps']['GenericHardwareManager'][0]) + self.assertEqual(states.CLEANWAIT, response) + + @mock.patch('ironic.objects.Port.list_by_node_id', + spec_set=types.FunctionType) + @mock.patch.object(agent_client.AgentClient, 'execute_clean_step', + autospec=True) + def test_execute_clean_step_version_mismatch( + self, client_mock, list_ports_mock): + client_mock.return_value = { + 'command_status': 'RUNNING'} + list_ports_mock.return_value = self.ports + + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + response = utils.agent_execute_clean_step( + task, + self.clean_steps['clean_steps']['GenericHardwareManager'][0]) + self.assertEqual(states.CLEANWAIT, response) + + def test_agent_add_clean_params(self): + cfg.CONF.deploy.erase_devices_iterations = 2 + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + utils.agent_add_clean_params(task) + self.assertEqual(task.node.driver_internal_info.get( + 'agent_erase_devices_iterations'), 2) + + @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.delete_cleaning_ports', + autospec=True) + @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.create_cleaning_ports', + autospec=True) + def _test_prepare_inband_cleaning_ports( + self, create_mock, delete_mock, return_vif_port_id=True): + if return_vif_port_id: + create_mock.return_value = {self.ports[0].uuid: 'vif-port-id'} + else: + create_mock.return_value = {} + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + utils.prepare_cleaning_ports(task) + create_mock.assert_called_once_with(mock.ANY, task) + delete_mock.assert_called_once_with(mock.ANY, task) + + self.ports[0].refresh() + self.assertEqual('vif-port-id', self.ports[0].extra['vif_port_id']) + + def test_prepare_inband_cleaning_ports(self): + self._test_prepare_inband_cleaning_ports() + + def test_prepare_inband_cleaning_ports_no_vif_port_id(self): + self.assertRaises( + exception.NodeCleaningFailure, + self._test_prepare_inband_cleaning_ports, + return_vif_port_id=False) + + @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.delete_cleaning_ports', + autospec=True) + def test_tear_down_inband_cleaning_ports(self, neutron_mock): + extra_dict = self.ports[0].extra + extra_dict['vif_port_id'] = 'vif-port-id' + self.ports[0].extra = extra_dict + self.ports[0].save() + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + utils.tear_down_cleaning_ports(task) + neutron_mock.assert_called_once_with(mock.ANY, task) + + self.ports[0].refresh() + self.assertNotIn('vif_port_id', self.ports[0].extra) + + @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', autospec=True) + @mock.patch('ironic.conductor.utils.node_power_action', autospec=True) + @mock.patch.object(utils, 'build_agent_options', autospec=True) + @mock.patch.object(utils, 'prepare_cleaning_ports', autospec=True) + def _test_prepare_inband_cleaning( + self, prepare_cleaning_ports_mock, + build_options_mock, power_mock, prepare_ramdisk_mock, + manage_boot=True): + build_options_mock.return_value = {'a': 'b'} + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + self.assertEqual( + states.CLEANWAIT, + utils.prepare_inband_cleaning(task, manage_boot=manage_boot)) + prepare_cleaning_ports_mock.assert_called_once_with(task) + power_mock.assert_called_once_with(task, states.REBOOT) + self.assertEqual(task.node.driver_internal_info.get( + 'agent_erase_devices_iterations'), 1) + if manage_boot: + prepare_ramdisk_mock.assert_called_once_with( + mock.ANY, mock.ANY, {'a': 'b'}) + build_options_mock.assert_called_once_with(task.node) + else: + self.assertFalse(prepare_ramdisk_mock.called) + self.assertFalse(build_options_mock.called) + + def test_prepare_inband_cleaning(self): + self._test_prepare_inband_cleaning() + + def test_prepare_inband_cleaning_manage_boot_false(self): + self._test_prepare_inband_cleaning(manage_boot=False) + + @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True) + @mock.patch.object(utils, 'tear_down_cleaning_ports', autospec=True) + @mock.patch('ironic.conductor.utils.node_power_action', autospec=True) + def _test_tear_down_inband_cleaning( + self, power_mock, tear_down_ports_mock, + clean_up_ramdisk_mock, manage_boot=True): + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + utils.tear_down_inband_cleaning(task, manage_boot=manage_boot) + power_mock.assert_called_once_with(task, states.POWER_OFF) + tear_down_ports_mock.assert_called_once_with(task) + if manage_boot: + clean_up_ramdisk_mock.assert_called_once_with( + task.driver.boot, task) + else: + self.assertFalse(clean_up_ramdisk_mock.called) + + def test_tear_down_inband_cleaning(self): + self._test_tear_down_inband_cleaning(manage_boot=True) + + def test_tear_down_inband_cleaning_manage_boot_false(self): + self._test_tear_down_inband_cleaning(manage_boot=False) + + def test_build_agent_options_conf(self): + self.config(api_url='api-url', group='conductor') + options = utils.build_agent_options(self.node) + self.assertEqual('api-url', options['ipa-api-url']) + self.assertEqual('fake_agent', options['ipa-driver-name']) + self.assertEqual(0, options['coreos.configdrive']) + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + def test_build_agent_options_keystone(self, get_url_mock): + + self.config(api_url=None, group='conductor') + get_url_mock.return_value = 'api-url' + options = utils.build_agent_options(self.node) + self.assertEqual('api-url', options['ipa-api-url']) + self.assertEqual('fake_agent', options['ipa-driver-name']) + self.assertEqual(0, options['coreos.configdrive']) + + def test_build_agent_options_root_device_hints(self): + self.config(api_url='api-url', group='conductor') + self.node.properties['root_device'] = {'model': 'fake_model'} + options = utils.build_agent_options(self.node) + self.assertEqual('api-url', options['ipa-api-url']) + self.assertEqual('fake_agent', options['ipa-driver-name']) + self.assertEqual('model=fake_model', options['root_device']) + + +@mock.patch.object(utils, 'is_block_device', autospec=True) +@mock.patch.object(utils, 'login_iscsi', lambda *_: None) +@mock.patch.object(utils, 'discovery', lambda *_: None) +@mock.patch.object(utils, 'logout_iscsi', lambda *_: None) +@mock.patch.object(utils, 'delete_iscsi', lambda *_: None) +@mock.patch.object(utils, 'get_dev', lambda *_: '/dev/fake') +class ISCSISetupAndHandleErrorsTestCase(tests_base.TestCase): + + def test_no_parent_device(self, mock_ibd): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + mock_ibd.return_value = False + expected_dev = '/dev/fake' + with testtools.ExpectedException(exception.InstanceDeployFailure): + with utils._iscsi_setup_and_handle_errors( + address, port, iqn, lun) as dev: + self.assertEqual(expected_dev, dev) + + mock_ibd.assert_called_once_with(expected_dev) + + def test_parent_device_yield(self, mock_ibd): + address = '127.0.0.1' + port = 3306 + iqn = 'iqn.xyz' + lun = 1 + expected_dev = '/dev/fake' + mock_ibd.return_value = True + with utils._iscsi_setup_and_handle_errors( + address, port, iqn, lun) as dev: + self.assertEqual(expected_dev, dev) + + mock_ibd.assert_called_once_with(expected_dev) + + +class ValidateImagePropertiesTestCase(db_base.DbTestCase): + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test_validate_image_properties_glance_image(self, image_service_mock): + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + d_info = pxe._parse_instance_info(node) + image_service_mock.return_value.show.return_value = { + 'properties': {'kernel_id': '1111', 'ramdisk_id': '2222'}, + } + + utils.validate_image_properties(self.context, d_info, + ['kernel_id', 'ramdisk_id']) + image_service_mock.assert_called_once_with( + node.instance_info['image_source'], context=self.context + ) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test_validate_image_properties_glance_image_missing_prop( + self, image_service_mock): + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + d_info = pxe._parse_instance_info(node) + image_service_mock.return_value.show.return_value = { + 'properties': {'kernel_id': '1111'}, + } + + self.assertRaises(exception.MissingParameterValue, + utils.validate_image_properties, + self.context, d_info, ['kernel_id', 'ramdisk_id']) + image_service_mock.assert_called_once_with( + node.instance_info['image_source'], context=self.context + ) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test_validate_image_properties_glance_image_not_authorized( + self, image_service_mock): + d_info = {'image_source': 'uuid'} + show_mock = image_service_mock.return_value.show + show_mock.side_effect = exception.ImageNotAuthorized(image_id='uuid') + self.assertRaises(exception.InvalidParameterValue, + utils.validate_image_properties, self.context, + d_info, []) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test_validate_image_properties_glance_image_not_found( + self, image_service_mock): + d_info = {'image_source': 'uuid'} + show_mock = image_service_mock.return_value.show + show_mock.side_effect = exception.ImageNotFound(image_id='uuid') + self.assertRaises(exception.InvalidParameterValue, + utils.validate_image_properties, self.context, + d_info, []) + + def test_validate_image_properties_invalid_image_href(self): + d_info = {'image_source': 'emule://uuid'} + self.assertRaises(exception.InvalidParameterValue, + utils.validate_image_properties, self.context, + d_info, []) + + @mock.patch.object(image_service.HttpImageService, 'show', autospec=True) + def test_validate_image_properties_nonglance_image( + self, image_service_show_mock): + instance_info = { + 'image_source': 'http://ubuntu', + 'kernel': 'kernel_uuid', + 'ramdisk': 'file://initrd', + 'root_gb': 100, + } + image_service_show_mock.return_value = {'size': 1, 'properties': {}} + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + d_info = pxe._parse_instance_info(node) + utils.validate_image_properties(self.context, d_info, + ['kernel', 'ramdisk']) + image_service_show_mock.assert_called_once_with( + mock.ANY, instance_info['image_source']) + + @mock.patch.object(image_service.HttpImageService, 'show', autospec=True) + def test_validate_image_properties_nonglance_image_validation_fail( + self, img_service_show_mock): + instance_info = { + 'image_source': 'http://ubuntu', + 'kernel': 'kernel_uuid', + 'ramdisk': 'file://initrd', + 'root_gb': 100, + } + img_service_show_mock.side_effect = iter( + [exception.ImageRefValidationFailed( + image_href='http://ubuntu', reason='HTTPError')]) + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + d_info = pxe._parse_instance_info(node) + self.assertRaises(exception.InvalidParameterValue, + utils.validate_image_properties, self.context, + d_info, ['kernel', 'ramdisk']) diff --git a/ironic/tests/unit/drivers/modules/test_iboot.py b/ironic/tests/unit/drivers/modules/test_iboot.py new file mode 100644 index 000000000..2ad9ed09f --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_iboot.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2014 Red Hat, Inc. +# 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. + +"""Test class for iBoot PDU driver module.""" + +import types + +import mock + +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import iboot +from ironic.tests.unit.conductor import utils as mgr_utils +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 + + +INFO_DICT = db_utils.get_test_iboot_info() + + +class IBootPrivateMethodTestCase(db_base.DbTestCase): + + def setUp(self): + super(IBootPrivateMethodTestCase, self).setUp() + self.config(max_retry=0, group='iboot') + self.config(retry_interval=0, group='iboot') + + def test__parse_driver_info_good(self): + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + self.assertIsNotNone(info.get('address')) + self.assertIsNotNone(info.get('username')) + self.assertIsNotNone(info.get('password')) + self.assertIsNotNone(info.get('port')) + self.assertIsNotNone(info.get('relay_id')) + + def test__parse_driver_info_good_with_explicit_port(self): + info = dict(INFO_DICT) + info['iboot_port'] = '1234' + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=info) + info = iboot._parse_driver_info(node) + self.assertEqual(1234, info.get('port')) + + def test__parse_driver_info_good_with_explicit_relay_id(self): + info = dict(INFO_DICT) + info['iboot_relay_id'] = '2' + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=info) + info = iboot._parse_driver_info(node) + self.assertEqual(2, info.get('relay_id')) + + def test__parse_driver_info_missing_address(self): + info = dict(INFO_DICT) + del info['iboot_address'] + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=info) + self.assertRaises(exception.MissingParameterValue, + iboot._parse_driver_info, + node) + + def test__parse_driver_info_missing_username(self): + info = dict(INFO_DICT) + del info['iboot_username'] + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=info) + self.assertRaises(exception.MissingParameterValue, + iboot._parse_driver_info, + node) + + def test__parse_driver_info_missing_password(self): + info = dict(INFO_DICT) + del info['iboot_password'] + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=info) + self.assertRaises(exception.MissingParameterValue, + iboot._parse_driver_info, + node) + + def test__parse_driver_info_bad_port(self): + info = dict(INFO_DICT) + info['iboot_port'] = 'not-integer' + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=info) + self.assertRaises(exception.InvalidParameterValue, + iboot._parse_driver_info, + node) + + def test__parse_driver_info_bad_relay_id(self): + info = dict(INFO_DICT) + info['iboot_relay_id'] = 'not-integer' + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=info) + self.assertRaises(exception.InvalidParameterValue, + iboot._parse_driver_info, + node) + + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__power_status_on(self, mock_get_conn): + mock_connection = mock.MagicMock(spec_set=['get_relays']) + mock_connection.get_relays.return_value = [True] + mock_get_conn.return_value = mock_connection + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + + status = iboot._power_status(info) + + self.assertEqual(states.POWER_ON, status) + mock_get_conn.assert_called_once_with(info) + mock_connection.get_relays.assert_called_once_with() + + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__power_status_off(self, mock_get_conn): + mock_connection = mock.MagicMock(spec_set=['get_relays']) + mock_connection.get_relays.return_value = [False] + mock_get_conn.return_value = mock_connection + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + + status = iboot._power_status(info) + + self.assertEqual(states.POWER_OFF, status) + mock_get_conn.assert_called_once_with(info) + mock_connection.get_relays.assert_called_once_with() + + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__power_status_exception(self, mock_get_conn): + mock_connection = mock.MagicMock(spec_set=['get_relays']) + mock_connection.get_relays.return_value = None + mock_get_conn.return_value = mock_connection + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + + status = iboot._power_status(info) + self.assertEqual(states.ERROR, status) + mock_get_conn.assert_called_once_with(info) + mock_connection.get_relays.assert_called_once_with() + + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__power_status_exception_type_error(self, mock_get_conn): + mock_connection = mock.MagicMock(spec_set=['get_relays']) + side_effect = TypeError("Surprise!") + mock_connection.get_relays.side_effect = side_effect + + mock_get_conn.return_value = mock_connection + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + + status = iboot._power_status(info) + self.assertEqual(states.ERROR, status) + mock_get_conn.assert_called_once_with(info) + mock_connection.get_relays.assert_called_once_with() + + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__power_status_exception_index_error(self, mock_get_conn): + mock_connection = mock.MagicMock(spec_set=['get_relays']) + side_effect = IndexError("Gotcha!") + mock_connection.get_relays.side_effect = side_effect + + mock_get_conn.return_value = mock_connection + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + status = iboot._power_status(info) + self.assertEqual(states.ERROR, status) + + mock_get_conn.assert_called_once_with(info) + mock_connection.get_relays.assert_called_once_with() + + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__power_status_error(self, mock_get_conn): + mock_connection = mock.MagicMock(spec_set=['get_relays']) + mock_connection.get_relays.return_value = list() + mock_get_conn.return_value = mock_connection + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + + status = iboot._power_status(info) + + self.assertEqual(states.ERROR, status) + mock_get_conn.assert_called_once_with(info) + mock_connection.get_relays.assert_called_once_with() + + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__power_status_retries(self, mock_get_conn): + self.config(max_retry=1, group='iboot') + + mock_connection = mock.MagicMock(spec_set=['get_relays']) + side_effect = TypeError("Surprise!") + mock_connection.get_relays.side_effect = side_effect + + mock_get_conn.return_value = mock_connection + node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + info = iboot._parse_driver_info(node) + + status = iboot._power_status(info) + self.assertEqual(states.ERROR, status) + mock_get_conn.assert_called_once_with(info) + self.assertEqual(2, mock_connection.get_relays.call_count) + + +class IBootDriverTestCase(db_base.DbTestCase): + + def setUp(self): + super(IBootDriverTestCase, self).setUp() + self.config(max_retry=0, group='iboot') + self.config(retry_interval=0, group='iboot') + mgr_utils.mock_the_extension_manager(driver='fake_iboot') + self.driver = driver_factory.get_driver('fake_iboot') + self.node = obj_utils.create_test_node( + self.context, + driver='fake_iboot', + driver_info=INFO_DICT) + self.info = iboot._parse_driver_info(self.node) + + def test_get_properties(self): + expected = iboot.COMMON_PROPERTIES + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(iboot, '_power_status', autospec=True) + @mock.patch.object(iboot, '_switch', autospec=True) + def test_set_power_state_good(self, mock_switch, mock_power_status): + mock_power_status.return_value = states.POWER_ON + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + + # ensure functions were called with the valid parameters + mock_switch.assert_called_once_with(self.info, True) + mock_power_status.assert_called_once_with(self.info) + + @mock.patch.object(iboot, '_power_status', autospec=True) + @mock.patch.object(iboot, '_switch', autospec=True) + def test_set_power_state_bad(self, mock_switch, mock_power_status): + mock_power_status.return_value = states.POWER_OFF + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.set_power_state, + task, states.POWER_ON) + + # ensure functions were called with the valid parameters + mock_switch.assert_called_once_with(self.info, True) + mock_power_status.assert_called_once_with(self.info) + + @mock.patch.object(iboot, '_power_status', autospec=True) + @mock.patch.object(iboot, '_switch', autospec=True) + def test_set_power_state_invalid_parameter(self, mock_switch, + mock_power_status): + mock_power_status.return_value = states.POWER_ON + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, states.NOSTATE) + + @mock.patch.object(iboot, '_power_status', autospec=True) + @mock.patch.object(iboot, '_switch', spec_set=types.FunctionType) + def test_reboot_good(self, mock_switch, mock_power_status): + manager = mock.MagicMock(spec_set=['switch']) + mock_power_status.return_value = states.POWER_ON + + manager.attach_mock(mock_switch, 'switch') + expected = [mock.call.switch(self.info, False), + mock.call.switch(self.info, True)] + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.reboot(task) + + self.assertEqual(manager.mock_calls, expected) + + @mock.patch.object(iboot, '_power_status', autospec=True) + @mock.patch.object(iboot, '_switch', spec_set=types.FunctionType) + def test_reboot_bad(self, mock_switch, mock_power_status): + manager = mock.MagicMock(spec_set=['switch']) + mock_power_status.return_value = states.POWER_OFF + + manager.attach_mock(mock_switch, 'switch') + expected = [mock.call.switch(self.info, False), + mock.call.switch(self.info, True)] + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.reboot, task) + + self.assertEqual(manager.mock_calls, expected) + + @mock.patch.object(iboot, '_power_status', autospec=True) + @mock.patch.object(iboot, '_get_connection', autospec=True) + def test__switch_retries(self, mock_get_conn, mock_power_status): + self.config(max_retry=1, group='iboot') + mock_power_status.return_value = states.POWER_ON + + mock_connection = mock.MagicMock(spec_set=['switch']) + side_effect = TypeError("Surprise!") + mock_connection.switch.side_effect = side_effect + mock_get_conn.return_value = mock_connection + + iboot._switch(self.info, False) + self.assertEqual(2, mock_connection.switch.call_count) + + @mock.patch.object(iboot, '_power_status', autospec=True) + def test_get_power_state(self, mock_power_status): + mock_power_status.return_value = states.POWER_ON + + with task_manager.acquire(self.context, self.node.uuid) as task: + state = task.driver.power.get_power_state(task) + self.assertEqual(state, states.POWER_ON) + + # ensure functions were called with the valid parameters + mock_power_status.assert_called_once_with(self.info) + + @mock.patch.object(iboot, '_parse_driver_info', autospec=True) + def test_validate_good(self, parse_drv_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.power.validate(task) + self.assertEqual(1, parse_drv_info_mock.call_count) + + @mock.patch.object(iboot, '_parse_driver_info', autospec=True) + def test_validate_fails(self, parse_drv_info_mock): + side_effect = iter([exception.InvalidParameterValue("Bad input")]) + parse_drv_info_mock.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, task) + self.assertEqual(1, parse_drv_info_mock.call_count) diff --git a/ironic/tests/unit/drivers/modules/test_image_cache.py b/ironic/tests/unit/drivers/modules/test_image_cache.py new file mode 100644 index 000000000..447db0a99 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_image_cache.py @@ -0,0 +1,697 @@ +# -*- encoding: utf-8 -*- +# +# Copyright 2014 Red Hat, Inc. +# +# 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 ImageCache class and helper functions.""" + +import datetime +import os +import tempfile +import time +import uuid + +import mock +from oslo_utils import uuidutils +import six + +from ironic.common import exception +from ironic.common import image_service +from ironic.common import images +from ironic.common import utils +from ironic.drivers.modules import image_cache +from ironic.tests.unit import base + + +def touch(filename): + open(filename, 'w').close() + + +class TestImageCacheFetch(base.TestCase): + + def setUp(self): + super(TestImageCacheFetch, self).setUp() + self.master_dir = tempfile.mkdtemp() + self.cache = image_cache.ImageCache(self.master_dir, None, None) + self.dest_dir = tempfile.mkdtemp() + self.dest_path = os.path.join(self.dest_dir, 'dest') + self.uuid = uuidutils.generate_uuid() + self.master_path = os.path.join(self.master_dir, self.uuid) + + @mock.patch.object(image_cache, '_fetch', autospec=True) + @mock.patch.object(image_cache.ImageCache, 'clean_up', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_download_image', + autospec=True) + def test_fetch_image_no_master_dir(self, mock_download, mock_clean_up, + mock_fetch): + self.cache.master_dir = None + self.cache.fetch_image(self.uuid, self.dest_path) + self.assertFalse(mock_download.called) + mock_fetch.assert_called_once_with( + None, self.uuid, self.dest_path, True) + self.assertFalse(mock_clean_up.called) + + @mock.patch.object(image_cache.ImageCache, 'clean_up', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_download_image', + autospec=True) + @mock.patch.object(os, 'link', autospec=True) + @mock.patch.object(image_cache, '_delete_dest_path_if_stale', + return_value=True, autospec=True) + @mock.patch.object(image_cache, '_delete_master_path_if_stale', + return_value=True, autospec=True) + def test_fetch_image_dest_and_master_uptodate( + self, mock_cache_upd, mock_dest_upd, mock_link, mock_download, + mock_clean_up): + self.cache.fetch_image(self.uuid, self.dest_path) + mock_cache_upd.assert_called_once_with(self.master_path, self.uuid, + None) + mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path) + self.assertFalse(mock_link.called) + self.assertFalse(mock_download.called) + self.assertFalse(mock_clean_up.called) + + @mock.patch.object(image_cache.ImageCache, 'clean_up', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_download_image', + autospec=True) + @mock.patch.object(os, 'link', autospec=True) + @mock.patch.object(image_cache, '_delete_dest_path_if_stale', + return_value=False, autospec=True) + @mock.patch.object(image_cache, '_delete_master_path_if_stale', + return_value=True, autospec=True) + def test_fetch_image_dest_out_of_date( + self, mock_cache_upd, mock_dest_upd, mock_link, mock_download, + mock_clean_up): + self.cache.fetch_image(self.uuid, self.dest_path) + mock_cache_upd.assert_called_once_with(self.master_path, self.uuid, + None) + mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path) + mock_link.assert_called_once_with(self.master_path, self.dest_path) + self.assertFalse(mock_download.called) + self.assertFalse(mock_clean_up.called) + + @mock.patch.object(image_cache.ImageCache, 'clean_up', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_download_image', + autospec=True) + @mock.patch.object(os, 'link', autospec=True) + @mock.patch.object(image_cache, '_delete_dest_path_if_stale', + return_value=True, autospec=True) + @mock.patch.object(image_cache, '_delete_master_path_if_stale', + return_value=False, autospec=True) + def test_fetch_image_master_out_of_date( + self, mock_cache_upd, mock_dest_upd, mock_link, mock_download, + mock_clean_up): + self.cache.fetch_image(self.uuid, self.dest_path) + mock_cache_upd.assert_called_once_with(self.master_path, self.uuid, + None) + mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path) + self.assertFalse(mock_link.called) + mock_download.assert_called_once_with( + self.cache, self.uuid, self.master_path, self.dest_path, + ctx=None, force_raw=True) + mock_clean_up.assert_called_once_with(self.cache) + + @mock.patch.object(image_cache.ImageCache, 'clean_up', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_download_image', + autospec=True) + @mock.patch.object(os, 'link', autospec=True) + @mock.patch.object(image_cache, '_delete_dest_path_if_stale', + return_value=True, autospec=True) + @mock.patch.object(image_cache, '_delete_master_path_if_stale', + return_value=False, autospec=True) + def test_fetch_image_both_master_and_dest_out_of_date( + self, mock_cache_upd, mock_dest_upd, mock_link, mock_download, + mock_clean_up): + self.cache.fetch_image(self.uuid, self.dest_path) + mock_cache_upd.assert_called_once_with(self.master_path, self.uuid, + None) + mock_dest_upd.assert_called_once_with(self.master_path, self.dest_path) + self.assertFalse(mock_link.called) + mock_download.assert_called_once_with( + self.cache, self.uuid, self.master_path, self.dest_path, + ctx=None, force_raw=True) + mock_clean_up.assert_called_once_with(self.cache) + + @mock.patch.object(image_cache.ImageCache, 'clean_up', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_download_image', + autospec=True) + def test_fetch_image_not_uuid(self, mock_download, mock_clean_up): + href = u'http://abc.com/ubuntu.qcow2' + href_encoded = href.encode('utf-8') if six.PY2 else href + href_converted = str(uuid.uuid5(uuid.NAMESPACE_URL, href_encoded)) + master_path = os.path.join(self.master_dir, href_converted) + self.cache.fetch_image(href, self.dest_path) + mock_download.assert_called_once_with( + self.cache, href, master_path, self.dest_path, + ctx=None, force_raw=True) + self.assertTrue(mock_clean_up.called) + + @mock.patch.object(image_cache, '_fetch', autospec=True) + def test__download_image(self, mock_fetch): + def _fake_fetch(ctx, uuid, tmp_path, *args): + self.assertEqual(self.uuid, uuid) + self.assertNotEqual(self.dest_path, tmp_path) + self.assertNotEqual(os.path.dirname(tmp_path), self.master_dir) + with open(tmp_path, 'w') as fp: + fp.write("TEST") + + mock_fetch.side_effect = _fake_fetch + self.cache._download_image(self.uuid, self.master_path, self.dest_path) + self.assertTrue(os.path.isfile(self.dest_path)) + self.assertTrue(os.path.isfile(self.master_path)) + self.assertEqual(os.stat(self.dest_path).st_ino, + os.stat(self.master_path).st_ino) + with open(self.dest_path) as fp: + self.assertEqual("TEST", fp.read()) + + +@mock.patch.object(os, 'unlink', autospec=True) +class TestUpdateImages(base.TestCase): + + def setUp(self): + super(TestUpdateImages, self).setUp() + self.master_dir = tempfile.mkdtemp() + self.dest_dir = tempfile.mkdtemp() + self.dest_path = os.path.join(self.dest_dir, 'dest') + self.uuid = uuidutils.generate_uuid() + self.master_path = os.path.join(self.master_dir, self.uuid) + + @mock.patch.object(os.path, 'exists', return_value=False, autospec=True) + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test__delete_master_path_if_stale_glance_img_not_cached( + self, mock_gis, mock_path_exists, mock_unlink): + res = image_cache._delete_master_path_if_stale(self.master_path, + self.uuid, None) + self.assertFalse(mock_gis.called) + self.assertFalse(mock_unlink.called) + mock_path_exists.assert_called_once_with(self.master_path) + self.assertFalse(res) + + @mock.patch.object(os.path, 'exists', return_value=True, autospec=True) + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test__delete_master_path_if_stale_glance_img( + self, mock_gis, mock_path_exists, mock_unlink): + res = image_cache._delete_master_path_if_stale(self.master_path, + self.uuid, None) + self.assertFalse(mock_gis.called) + self.assertFalse(mock_unlink.called) + mock_path_exists.assert_called_once_with(self.master_path) + self.assertTrue(res) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test__delete_master_path_if_stale_no_master(self, mock_gis, + mock_unlink): + res = image_cache._delete_master_path_if_stale(self.master_path, + 'http://11', None) + self.assertFalse(mock_gis.called) + self.assertFalse(mock_unlink.called) + self.assertFalse(res) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test__delete_master_path_if_stale_no_updated_at(self, mock_gis, + mock_unlink): + touch(self.master_path) + href = 'http://awesomefreeimages.al/img111' + mock_gis.return_value.show.return_value = {} + res = image_cache._delete_master_path_if_stale(self.master_path, href, + None) + mock_gis.assert_called_once_with(href, context=None) + self.assertFalse(mock_unlink.called) + self.assertTrue(res) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test__delete_master_path_if_stale_master_up_to_date(self, mock_gis, + mock_unlink): + touch(self.master_path) + href = 'http://awesomefreeimages.al/img999' + mock_gis.return_value.show.return_value = { + 'updated_at': datetime.datetime(1999, 11, 15, 8, 12, 31) + } + res = image_cache._delete_master_path_if_stale(self.master_path, href, + None) + mock_gis.assert_called_once_with(href, context=None) + self.assertFalse(mock_unlink.called) + self.assertTrue(res) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test__delete_master_path_if_stale_master_same_time(self, mock_gis, + mock_unlink): + # When times identical should not delete cached file + touch(self.master_path) + mtime = utils.unix_file_modification_datetime(self.master_path) + href = 'http://awesomefreeimages.al/img999' + mock_gis.return_value.show.return_value = { + 'updated_at': mtime + } + res = image_cache._delete_master_path_if_stale(self.master_path, href, + None) + mock_gis.assert_called_once_with(href, context=None) + self.assertFalse(mock_unlink.called) + self.assertTrue(res) + + @mock.patch.object(image_service, 'get_image_service', autospec=True) + def test__delete_master_path_if_stale_out_of_date(self, mock_gis, + mock_unlink): + touch(self.master_path) + href = 'http://awesomefreeimages.al/img999' + mock_gis.return_value.show.return_value = { + 'updated_at': datetime.datetime((datetime.datetime.utcnow().year + + 1), 11, 15, 8, 12, 31) + } + res = image_cache._delete_master_path_if_stale(self.master_path, href, + None) + mock_gis.assert_called_once_with(href, context=None) + mock_unlink.assert_called_once_with(self.master_path) + self.assertFalse(res) + + def test__delete_dest_path_if_stale_no_dest(self, mock_unlink): + res = image_cache._delete_dest_path_if_stale(self.master_path, + self.dest_path) + self.assertFalse(mock_unlink.called) + self.assertFalse(res) + + def test__delete_dest_path_if_stale_no_master(self, mock_unlink): + touch(self.dest_path) + res = image_cache._delete_dest_path_if_stale(self.master_path, + self.dest_path) + mock_unlink.assert_called_once_with(self.dest_path) + self.assertFalse(res) + + def test__delete_dest_path_if_stale_out_of_date(self, mock_unlink): + touch(self.master_path) + touch(self.dest_path) + res = image_cache._delete_dest_path_if_stale(self.master_path, + self.dest_path) + mock_unlink.assert_called_once_with(self.dest_path) + self.assertFalse(res) + + def test__delete_dest_path_if_stale_up_to_date(self, mock_unlink): + touch(self.master_path) + os.link(self.master_path, self.dest_path) + res = image_cache._delete_dest_path_if_stale(self.master_path, + self.dest_path) + self.assertFalse(mock_unlink.called) + self.assertTrue(res) + + +class TestImageCacheCleanUp(base.TestCase): + + def setUp(self): + super(TestImageCacheCleanUp, self).setUp() + self.master_dir = tempfile.mkdtemp() + self.cache = image_cache.ImageCache(self.master_dir, + cache_size=10, + cache_ttl=600) + + @mock.patch.object(image_cache.ImageCache, '_clean_up_ensure_cache_size', + autospec=True) + def test_clean_up_old_deleted(self, mock_clean_size): + mock_clean_size.return_value = None + files = [os.path.join(self.master_dir, str(i)) + for i in range(2)] + for filename in files: + touch(filename) + # NOTE(dtantsur): Can't alter ctime, have to set mtime to the future + new_current_time = time.time() + 900 + os.utime(files[0], (new_current_time - 100, new_current_time - 100)) + with mock.patch.object(time, 'time', lambda: new_current_time): + self.cache.clean_up() + + mock_clean_size.assert_called_once_with(self.cache, mock.ANY, None) + survived = mock_clean_size.call_args[0][1] + self.assertEqual(1, len(survived)) + self.assertEqual(files[0], survived[0][0]) + # NOTE(dtantsur): do not compare milliseconds + self.assertEqual(int(new_current_time - 100), int(survived[0][1])) + self.assertEqual(int(new_current_time - 100), + int(survived[0][2].st_mtime)) + + @mock.patch.object(image_cache.ImageCache, '_clean_up_ensure_cache_size', + autospec=True) + def test_clean_up_old_with_amount(self, mock_clean_size): + files = [os.path.join(self.master_dir, str(i)) + for i in range(2)] + for filename in files: + open(filename, 'wb').write(b'X') + new_current_time = time.time() + 900 + with mock.patch.object(time, 'time', lambda: new_current_time): + self.cache.clean_up(amount=1) + + self.assertFalse(mock_clean_size.called) + # Exactly one file is expected to be deleted + self.assertTrue(any(os.path.exists(f) for f in files)) + self.assertFalse(all(os.path.exists(f) for f in files)) + + @mock.patch.object(image_cache.ImageCache, '_clean_up_ensure_cache_size', + autospec=True) + def test_clean_up_files_with_links_untouched(self, mock_clean_size): + mock_clean_size.return_value = None + files = [os.path.join(self.master_dir, str(i)) + for i in range(2)] + for filename in files: + touch(filename) + os.link(filename, filename + 'copy') + + new_current_time = time.time() + 900 + with mock.patch.object(time, 'time', lambda: new_current_time): + self.cache.clean_up() + + for filename in files: + self.assertTrue(os.path.exists(filename)) + mock_clean_size.assert_called_once_with(mock.ANY, [], None) + + @mock.patch.object(image_cache.ImageCache, '_clean_up_too_old', + autospec=True) + def test_clean_up_ensure_cache_size(self, mock_clean_ttl): + mock_clean_ttl.side_effect = lambda *xx: xx[1:] + # NOTE(dtantsur): Cache size in test is 10 bytes, we create 6 files + # with 3 bytes each and expect 3 to be deleted + files = [os.path.join(self.master_dir, str(i)) + for i in range(6)] + for filename in files: + with open(filename, 'w') as fp: + fp.write('123') + # NOTE(dtantsur): Make 3 files 'newer' to check that + # old ones are deleted first + new_current_time = time.time() + 100 + for filename in files[:3]: + os.utime(filename, (new_current_time, new_current_time)) + + with mock.patch.object(time, 'time', lambda: new_current_time): + self.cache.clean_up() + + for filename in files[:3]: + self.assertTrue(os.path.exists(filename)) + for filename in files[3:]: + self.assertFalse(os.path.exists(filename)) + + mock_clean_ttl.assert_called_once_with(mock.ANY, mock.ANY, None) + + @mock.patch.object(image_cache.ImageCache, '_clean_up_too_old', + autospec=True) + def test_clean_up_ensure_cache_size_with_amount(self, mock_clean_ttl): + mock_clean_ttl.side_effect = lambda *xx: xx[1:] + # NOTE(dtantsur): Cache size in test is 10 bytes, we create 6 files + # with 3 bytes each and set amount to be 15, 5 files are to be deleted + files = [os.path.join(self.master_dir, str(i)) + for i in range(6)] + for filename in files: + with open(filename, 'w') as fp: + fp.write('123') + # NOTE(dtantsur): Make 1 file 'newer' to check that + # old ones are deleted first + new_current_time = time.time() + 100 + os.utime(files[0], (new_current_time, new_current_time)) + + with mock.patch.object(time, 'time', lambda: new_current_time): + self.cache.clean_up(amount=15) + + self.assertTrue(os.path.exists(files[0])) + for filename in files[5:]: + self.assertFalse(os.path.exists(filename)) + + mock_clean_ttl.assert_called_once_with(mock.ANY, mock.ANY, 15) + + @mock.patch.object(image_cache.LOG, 'info', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_clean_up_too_old', + autospec=True) + def test_clean_up_cache_still_large(self, mock_clean_ttl, mock_log): + mock_clean_ttl.side_effect = lambda *xx: xx[1:] + # NOTE(dtantsur): Cache size in test is 10 bytes, we create 2 files + # than cannot be deleted and expected this to be logged + files = [os.path.join(self.master_dir, str(i)) + for i in range(2)] + for filename in files: + with open(filename, 'w') as fp: + fp.write('123') + os.link(filename, filename + 'copy') + + self.cache.clean_up() + + for filename in files: + self.assertTrue(os.path.exists(filename)) + self.assertTrue(mock_log.called) + mock_clean_ttl.assert_called_once_with(mock.ANY, mock.ANY, None) + + @mock.patch.object(utils, 'rmtree_without_raise', autospec=True) + @mock.patch.object(image_cache, '_fetch', autospec=True) + def test_temp_images_not_cleaned(self, mock_fetch, mock_rmtree): + def _fake_fetch(ctx, uuid, tmp_path, *args): + with open(tmp_path, 'w') as fp: + fp.write("TEST" * 10) + + # assume cleanup from another thread at this moment + self.cache.clean_up() + self.assertTrue(os.path.exists(tmp_path)) + + mock_fetch.side_effect = _fake_fetch + master_path = os.path.join(self.master_dir, 'uuid') + dest_path = os.path.join(tempfile.mkdtemp(), 'dest') + self.cache._download_image('uuid', master_path, dest_path) + self.assertTrue(mock_rmtree.called) + + @mock.patch.object(utils, 'rmtree_without_raise', autospec=True) + @mock.patch.object(image_cache, '_fetch', autospec=True) + def test_temp_dir_exception(self, mock_fetch, mock_rmtree): + mock_fetch.side_effect = exception.IronicException + self.assertRaises(exception.IronicException, + self.cache._download_image, + 'uuid', 'fake', 'fake') + self.assertTrue(mock_rmtree.called) + + @mock.patch.object(image_cache.LOG, 'warn', autospec=True) + @mock.patch.object(image_cache.ImageCache, '_clean_up_too_old', + autospec=True) + @mock.patch.object(image_cache.ImageCache, '_clean_up_ensure_cache_size', + autospec=True) + def test_clean_up_amount_not_satisfied(self, mock_clean_size, + mock_clean_ttl, mock_log): + mock_clean_ttl.side_effect = lambda *xx: xx[1:] + mock_clean_size.side_effect = lambda self, listing, amount: amount + self.cache.clean_up(amount=15) + self.assertTrue(mock_log.called) + + def test_cleanup_ordering(self): + + class ParentCache(image_cache.ImageCache): + def __init__(self): + super(ParentCache, self).__init__('a', 1, 1, None) + + @image_cache.cleanup(priority=10000) + class Cache1(ParentCache): + pass + + @image_cache.cleanup(priority=20000) + class Cache2(ParentCache): + pass + + @image_cache.cleanup(priority=10000) + class Cache3(ParentCache): + pass + + self.assertEqual(image_cache._cache_cleanup_list[0][1], Cache2) + + # The order of caches with same prioirty is not deterministic. + item_possibilities = [Cache1, Cache3] + second_item_actual = image_cache._cache_cleanup_list[1][1] + self.assertIn(second_item_actual, item_possibilities) + item_possibilities.remove(second_item_actual) + third_item_actual = image_cache._cache_cleanup_list[2][1] + self.assertEqual(item_possibilities[0], third_item_actual) + + +@mock.patch.object(image_cache, '_cache_cleanup_list', autospec=True) +@mock.patch.object(os, 'statvfs', autospec=True) +@mock.patch.object(image_service, 'get_image_service', autospec=True) +class CleanupImageCacheTestCase(base.TestCase): + + def setUp(self): + super(CleanupImageCacheTestCase, self).setUp() + self.mock_first_cache = mock.MagicMock(spec_set=[]) + self.mock_second_cache = mock.MagicMock(spec_set=[]) + self.cache_cleanup_list = [(50, self.mock_first_cache), + (20, self.mock_second_cache)] + self.mock_first_cache.return_value.master_dir = 'first_cache_dir' + self.mock_second_cache.return_value.master_dir = 'second_cache_dir' + + def test_no_clean_up(self, mock_image_service, mock_statvfs, + cache_cleanup_list_mock): + # Enough space found - no clean up + mock_show = mock_image_service.return_value.show + mock_show.return_value = dict(size=42) + mock_statvfs.return_value = mock.MagicMock( + spec_set=['f_frsize', 'f_bavail'], f_frsize=1, f_bavail=1024) + + cache_cleanup_list_mock.__iter__.return_value = self.cache_cleanup_list + + image_cache.clean_up_caches(None, 'master_dir', [('uuid', 'path')]) + + mock_show.assert_called_once_with('uuid') + mock_statvfs.assert_called_once_with('master_dir') + self.assertFalse(self.mock_first_cache.return_value.clean_up.called) + self.assertFalse(self.mock_second_cache.return_value.clean_up.called) + + mock_statvfs.assert_called_once_with('master_dir') + + @mock.patch.object(os, 'stat', autospec=True) + def test_one_clean_up(self, mock_stat, mock_image_service, mock_statvfs, + cache_cleanup_list_mock): + # Not enough space, first cache clean up is enough + mock_stat.return_value.st_dev = 1 + mock_show = mock_image_service.return_value.show + mock_show.return_value = dict(size=42) + mock_statvfs.side_effect = [ + mock.MagicMock(f_frsize=1, f_bavail=1, + spec_set=['f_frsize', 'f_bavail']), + mock.MagicMock(f_frsize=1, f_bavail=1024, + spec_set=['f_frsize', 'f_bavail']) + ] + cache_cleanup_list_mock.__iter__.return_value = self.cache_cleanup_list + image_cache.clean_up_caches(None, 'master_dir', [('uuid', 'path')]) + + mock_show.assert_called_once_with('uuid') + mock_statvfs.assert_called_with('master_dir') + self.assertEqual(2, mock_statvfs.call_count) + self.mock_first_cache.return_value.clean_up.assert_called_once_with( + amount=(42 - 1)) + self.assertFalse(self.mock_second_cache.return_value.clean_up.called) + + # Since we are using generator expression in clean_up_caches, stat on + # second cache wouldn't be called if we got enough free space on + # cleaning up the first cache. + mock_stat_calls_expected = [mock.call('master_dir'), + mock.call('first_cache_dir')] + mock_statvfs_calls_expected = [mock.call('master_dir'), + mock.call('master_dir')] + self.assertEqual(mock_stat_calls_expected, mock_stat.mock_calls) + self.assertEqual(mock_statvfs_calls_expected, mock_statvfs.mock_calls) + + @mock.patch.object(os, 'stat', autospec=True) + def test_clean_up_another_fs(self, mock_stat, mock_image_service, + mock_statvfs, cache_cleanup_list_mock): + # Not enough space, need to cleanup second cache + mock_stat.side_effect = [mock.MagicMock(st_dev=1, spec_set=['st_dev']), + mock.MagicMock(st_dev=2, spec_set=['st_dev']), + mock.MagicMock(st_dev=1, spec_set=['st_dev'])] + mock_show = mock_image_service.return_value.show + mock_show.return_value = dict(size=42) + mock_statvfs.side_effect = [ + mock.MagicMock(f_frsize=1, f_bavail=1, + spec_set=['f_frsize', 'f_bavail']), + mock.MagicMock(f_frsize=1, f_bavail=1024, + spec_set=['f_frsize', 'f_bavail']) + ] + + cache_cleanup_list_mock.__iter__.return_value = self.cache_cleanup_list + image_cache.clean_up_caches(None, 'master_dir', [('uuid', 'path')]) + + mock_show.assert_called_once_with('uuid') + mock_statvfs.assert_called_with('master_dir') + self.assertEqual(2, mock_statvfs.call_count) + self.mock_second_cache.return_value.clean_up.assert_called_once_with( + amount=(42 - 1)) + self.assertFalse(self.mock_first_cache.return_value.clean_up.called) + + # Since first cache exists on a different partition, it wouldn't be + # considered for cleanup. + mock_stat_calls_expected = [mock.call('master_dir'), + mock.call('first_cache_dir'), + mock.call('second_cache_dir')] + mock_statvfs_calls_expected = [mock.call('master_dir'), + mock.call('master_dir')] + self.assertEqual(mock_stat_calls_expected, mock_stat.mock_calls) + self.assertEqual(mock_statvfs_calls_expected, mock_statvfs.mock_calls) + + @mock.patch.object(os, 'stat', autospec=True) + def test_both_clean_up(self, mock_stat, mock_image_service, mock_statvfs, + cache_cleanup_list_mock): + # Not enough space, clean up of both caches required + mock_stat.return_value.st_dev = 1 + mock_show = mock_image_service.return_value.show + mock_show.return_value = dict(size=42) + mock_statvfs.side_effect = [ + mock.MagicMock(f_frsize=1, f_bavail=1, + spec_set=['f_frsize', 'f_bavail']), + mock.MagicMock(f_frsize=1, f_bavail=2, + spec_set=['f_frsize', 'f_bavail']), + mock.MagicMock(f_frsize=1, f_bavail=1024, + spec_set=['f_frsize', 'f_bavail']) + ] + + cache_cleanup_list_mock.__iter__.return_value = self.cache_cleanup_list + image_cache.clean_up_caches(None, 'master_dir', [('uuid', 'path')]) + + mock_show.assert_called_once_with('uuid') + mock_statvfs.assert_called_with('master_dir') + self.assertEqual(3, mock_statvfs.call_count) + self.mock_first_cache.return_value.clean_up.assert_called_once_with( + amount=(42 - 1)) + self.mock_second_cache.return_value.clean_up.assert_called_once_with( + amount=(42 - 2)) + + mock_stat_calls_expected = [mock.call('master_dir'), + mock.call('first_cache_dir'), + mock.call('second_cache_dir')] + mock_statvfs_calls_expected = [mock.call('master_dir'), + mock.call('master_dir'), + mock.call('master_dir')] + self.assertEqual(mock_stat_calls_expected, mock_stat.mock_calls) + self.assertEqual(mock_statvfs_calls_expected, mock_statvfs.mock_calls) + + @mock.patch.object(os, 'stat', autospec=True) + def test_clean_up_fail(self, mock_stat, mock_image_service, mock_statvfs, + cache_cleanup_list_mock): + # Not enough space even after cleaning both caches - failure + mock_stat.return_value.st_dev = 1 + mock_show = mock_image_service.return_value.show + mock_show.return_value = dict(size=42) + mock_statvfs.return_value = mock.MagicMock( + f_frsize=1, f_bavail=1, spec_set=['f_frsize', 'f_bavail']) + + cache_cleanup_list_mock.__iter__.return_value = self.cache_cleanup_list + self.assertRaises(exception.InsufficientDiskSpace, + image_cache.clean_up_caches, + None, 'master_dir', [('uuid', 'path')]) + + mock_show.assert_called_once_with('uuid') + mock_statvfs.assert_called_with('master_dir') + self.assertEqual(3, mock_statvfs.call_count) + self.mock_first_cache.return_value.clean_up.assert_called_once_with( + amount=(42 - 1)) + self.mock_second_cache.return_value.clean_up.assert_called_once_with( + amount=(42 - 1)) + + mock_stat_calls_expected = [mock.call('master_dir'), + mock.call('first_cache_dir'), + mock.call('second_cache_dir')] + mock_statvfs_calls_expected = [mock.call('master_dir'), + mock.call('master_dir'), + mock.call('master_dir')] + self.assertEqual(mock_stat_calls_expected, mock_stat.mock_calls) + self.assertEqual(mock_statvfs_calls_expected, mock_statvfs.mock_calls) + + +class TestFetchCleanup(base.TestCase): + + @mock.patch.object(images, 'converted_size', autospec=True) + @mock.patch.object(images, 'fetch', autospec=True) + @mock.patch.object(images, 'image_to_raw', autospec=True) + @mock.patch.object(image_cache, '_clean_up_caches', autospec=True) + def test__fetch(self, mock_clean, mock_raw, mock_fetch, mock_size): + mock_size.return_value = 100 + image_cache._fetch('fake', 'fake-uuid', '/foo/bar', force_raw=True) + mock_fetch.assert_called_once_with('fake', 'fake-uuid', + '/foo/bar.part', force_raw=False) + mock_clean.assert_called_once_with('/foo', 100) + mock_raw.assert_called_once_with('fake-uuid', '/foo/bar', + '/foo/bar.part') diff --git a/ironic/tests/unit/drivers/modules/test_inspector.py b/ironic/tests/unit/drivers/modules/test_inspector.py new file mode 100644 index 000000000..349acb4b8 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_inspector.py @@ -0,0 +1,239 @@ +# 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 eventlet +import ironic_inspector_client as client +import mock + +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import keystone +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import inspector +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + + +class DisabledTestCase(db_base.DbTestCase): + def setUp(self): + super(DisabledTestCase, self).setUp() + + def _do_mock(self): + # NOTE(dtantsur): fake driver always has inspection, using another one + mgr_utils.mock_the_extension_manager("pxe_ssh") + self.driver = driver_factory.get_driver("pxe_ssh") + + def test_disabled(self): + self.config(enabled=False, group='inspector') + self._do_mock() + self.assertIsNone(self.driver.inspect) + # NOTE(dtantsur): it's expected that fake_inspector fails to load + # in this case + self.assertRaises(exception.DriverLoadError, + mgr_utils.mock_the_extension_manager, + "fake_inspector") + + def test_enabled(self): + self.config(enabled=True, group='inspector') + self._do_mock() + self.assertIsNotNone(self.driver.inspect) + + @mock.patch.object(inspector, 'client', None) + def test_init_inspector_not_imported(self): + self.assertRaises(exception.DriverLoadError, + inspector.Inspector) + + def test_init_ok(self): + self.config(enabled=True, group='inspector') + inspector.Inspector() + + +class BaseTestCase(db_base.DbTestCase): + def setUp(self): + super(BaseTestCase, self).setUp() + self.config(enabled=True, group='inspector') + mgr_utils.mock_the_extension_manager("fake_inspector") + self.driver = driver_factory.get_driver("fake_inspector") + self.node = obj_utils.get_test_node(self.context) + self.task = mock.MagicMock(spec=task_manager.TaskManager) + self.task.context = mock.MagicMock(spec_set=['auth_token']) + self.task.shared = False + self.task.node = self.node + self.task.driver = self.driver + self.api_version = (1, 0) + + +class CommonFunctionsTestCase(BaseTestCase): + def test_validate_ok(self): + self.driver.inspect.validate(self.task) + + def test_get_properties(self): + res = self.driver.inspect.get_properties() + self.assertEqual({}, res) + + def test_create_if_enabled(self): + res = inspector.Inspector.create_if_enabled('driver') + self.assertIsInstance(res, inspector.Inspector) + + @mock.patch.object(inspector.LOG, 'info', autospec=True) + def test_create_if_enabled_disabled(self, warn_mock): + self.config(enabled=False, group='inspector') + res = inspector.Inspector.create_if_enabled('driver') + self.assertIsNone(res) + self.assertTrue(warn_mock.called) + + +@mock.patch.object(eventlet, 'spawn_n', lambda f, *a, **kw: f(*a, **kw)) +@mock.patch.object(client, 'introspect') +class InspectHardwareTestCase(BaseTestCase): + def test_ok(self, mock_introspect): + self.assertEqual(states.INSPECTING, + self.driver.inspect.inspect_hardware(self.task)) + mock_introspect.assert_called_once_with( + self.node.uuid, + api_version=self.api_version, + auth_token=self.task.context.auth_token) + + def test_url(self, mock_introspect): + self.config(service_url='meow', group='inspector') + self.assertEqual(states.INSPECTING, + self.driver.inspect.inspect_hardware(self.task)) + mock_introspect.assert_called_once_with( + self.node.uuid, + api_version=self.api_version, + auth_token=self.task.context.auth_token, + base_url='meow') + + @mock.patch.object(task_manager, 'acquire', autospec=True) + def test_error(self, mock_acquire, mock_introspect): + mock_introspect.side_effect = RuntimeError('boom') + self.driver.inspect.inspect_hardware(self.task) + mock_introspect.assert_called_once_with( + self.node.uuid, + api_version=self.api_version, + auth_token=self.task.context.auth_token) + task = mock_acquire.return_value.__enter__.return_value + self.assertIn('boom', task.node.last_error) + task.process_event.assert_called_once_with('fail') + + +@mock.patch.object(keystone, 'get_admin_auth_token', lambda: 'the token') +@mock.patch.object(client, 'get_status') +class CheckStatusTestCase(BaseTestCase): + def setUp(self): + super(CheckStatusTestCase, self).setUp() + self.node.provision_state = states.INSPECTING + + def test_not_inspecting(self, mock_get): + self.node.provision_state = states.MANAGEABLE + inspector._check_status(self.task) + self.assertFalse(mock_get.called) + + def test_not_inspector(self, mock_get): + self.task.driver.inspect = object() + inspector._check_status(self.task) + self.assertFalse(mock_get.called) + + def test_not_finished(self, mock_get): + mock_get.return_value = {} + inspector._check_status(self.task) + mock_get.assert_called_once_with(self.node.uuid, + api_version=self.api_version, + auth_token='the token') + self.assertFalse(self.task.process_event.called) + + def test_exception_ignored(self, mock_get): + mock_get.side_effect = RuntimeError('boom') + inspector._check_status(self.task) + mock_get.assert_called_once_with(self.node.uuid, + api_version=self.api_version, + auth_token='the token') + self.assertFalse(self.task.process_event.called) + + def test_status_ok(self, mock_get): + mock_get.return_value = {'finished': True} + inspector._check_status(self.task) + mock_get.assert_called_once_with(self.node.uuid, + api_version=self.api_version, + auth_token='the token') + self.task.process_event.assert_called_once_with('done') + + def test_status_error(self, mock_get): + mock_get.return_value = {'error': 'boom'} + inspector._check_status(self.task) + mock_get.assert_called_once_with(self.node.uuid, + api_version=self.api_version, + auth_token='the token') + self.task.process_event.assert_called_once_with('fail') + self.assertIn('boom', self.node.last_error) + + def test_service_url(self, mock_get): + self.config(service_url='meow', group='inspector') + mock_get.return_value = {'finished': True} + inspector._check_status(self.task) + mock_get.assert_called_once_with(self.node.uuid, + api_version=self.api_version, + auth_token='the token', + base_url='meow') + self.task.process_event.assert_called_once_with('done') + + def test_is_standalone(self, mock_get): + self.config(auth_strategy='noauth') + mock_get.return_value = {'finished': True} + inspector._check_status(self.task) + mock_get.assert_called_once_with( + self.node.uuid, + api_version=self.api_version, + auth_token=self.task.context.auth_token) + self.task.process_event.assert_called_once_with('done') + + def test_not_standalone(self, mock_get): + self.config(auth_strategy='keystone') + mock_get.return_value = {'finished': True} + inspector._check_status(self.task) + mock_get.assert_called_once_with(self.node.uuid, + api_version=self.api_version, + auth_token='the token') + self.task.process_event.assert_called_once_with('done') + + +@mock.patch.object(eventlet.greenthread, 'spawn_n', + lambda f, *a, **kw: f(*a, **kw)) +@mock.patch.object(task_manager, 'acquire', autospec=True) +@mock.patch.object(inspector, '_check_status', autospec=True) +class PeriodicTaskTestCase(BaseTestCase): + def test_ok(self, mock_check, mock_acquire): + mgr = mock.MagicMock(spec=['iter_nodes']) + mgr.iter_nodes.return_value = [('1', 'd1'), ('2', 'd2')] + tasks = [mock.sentinel.task1, mock.sentinel.task2] + mock_acquire.side_effect = ( + mock.MagicMock(__enter__=mock.MagicMock(return_value=task)) + for task in tasks + ) + inspector.Inspector()._periodic_check_result( + mgr, mock.sentinel.context) + mock_check.assert_any_call(tasks[0]) + mock_check.assert_any_call(tasks[1]) + self.assertEqual(2, mock_acquire.call_count) + + def test_node_locked(self, mock_check, mock_acquire): + iter_nodes_ret = [('1', 'd1'), ('2', 'd2')] + mock_acquire.side_effect = iter([exception.NodeLocked("boom")] * + len(iter_nodes_ret)) + mgr = mock.MagicMock(spec=['iter_nodes']) + mgr.iter_nodes.return_value = iter_nodes_ret + inspector.Inspector()._periodic_check_result( + mgr, mock.sentinel.context) + self.assertFalse(mock_check.called) + self.assertEqual(2, mock_acquire.call_count) diff --git a/ironic/tests/unit/drivers/modules/test_ipminative.py b/ironic/tests/unit/drivers/modules/test_ipminative.py new file mode 100644 index 000000000..8cc1ecf87 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_ipminative.py @@ -0,0 +1,610 @@ +# coding=utf-8 + +# Copyright 2013 International Business Machines Corporation +# 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. + +""" +Test class for Native IPMI power driver module. +""" + +import mock +from oslo_utils import uuidutils +from pyghmi import exceptions as pyghmi_exception + +from ironic.common import boot_devices +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import console_utils +from ironic.drivers.modules import ipminative +from ironic.drivers import utils as driver_utils +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INFO_DICT = db_utils.get_test_ipmi_info() + + +class IPMINativePrivateMethodTestCase(db_base.DbTestCase): + """Test cases for ipminative private methods.""" + + def setUp(self): + super(IPMINativePrivateMethodTestCase, self).setUp() + self.node = obj_utils.create_test_node(self.context, + driver='fake_ipminative', + driver_info=INFO_DICT) + self.info = ipminative._parse_driver_info(self.node) + + def test__parse_driver_info(self): + # make sure we get back the expected things + self.assertIsNotNone(self.info.get('address')) + self.assertIsNotNone(self.info.get('username')) + self.assertIsNotNone(self.info.get('password')) + self.assertIsNotNone(self.info.get('uuid')) + self.assertIsNotNone(self.info.get('force_boot_device')) + + # make sure error is raised when info, eg. username, is missing + info = dict(INFO_DICT) + del info['ipmi_username'] + + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + ipminative._parse_driver_info, + node) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__power_status_on(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_power.return_value = {'powerstate': 'on'} + + state = ipminative._power_status(self.info) + ipmicmd.get_power.assert_called_once_with() + self.assertEqual(states.POWER_ON, state) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__power_status_off(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_power.return_value = {'powerstate': 'off'} + + state = ipminative._power_status(self.info) + ipmicmd.get_power.assert_called_once_with() + self.assertEqual(states.POWER_OFF, state) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__power_status_error(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_power.return_value = {'powerstate': 'Error'} + + state = ipminative._power_status(self.info) + ipmicmd.get_power.assert_called_once_with() + self.assertEqual(states.ERROR, state) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__power_on(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_power.return_value = {'powerstate': 'on'} + + self.config(retry_timeout=400, group='ipmi') + state = ipminative._power_on(self.info) + ipmicmd.set_power.assert_called_once_with('on', 400) + self.assertEqual(states.POWER_ON, state) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__power_off(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_power.return_value = {'powerstate': 'off'} + + self.config(retry_timeout=500, group='ipmi') + state = ipminative._power_off(self.info) + ipmicmd.set_power.assert_called_once_with('off', 500) + self.assertEqual(states.POWER_OFF, state) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__reboot(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_power.return_value = {'powerstate': 'on'} + + self.config(retry_timeout=600, group='ipmi') + state = ipminative._reboot(self.info) + ipmicmd.set_power.assert_called_once_with('boot', 600) + self.assertEqual(states.POWER_ON, state) + + def _create_sensor_object(self, value, type_, name, states=None, + units='fake_units', health=0): + if states is None: + states = [] + return type('Reading', (object, ), { + 'value': value, 'type': type_, 'name': name, + 'states': states, 'units': units, 'health': health})() + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__get_sensors_data(self, ipmi_mock): + reading_1 = self._create_sensor_object('fake_value1', + 'fake_type_A', + 'fake_name1') + reading_2 = self._create_sensor_object('fake_value2', + 'fake_type_A', + 'fake_name2') + reading_3 = self._create_sensor_object('fake_value3', + 'fake_type_B', + 'fake_name3') + readings = [reading_1, reading_2, reading_3] + ipmicmd = ipmi_mock.return_value + ipmicmd.get_sensor_data.return_value = readings + expected = { + 'fake_type_A': { + 'fake_name1': { + 'Health': '0', + 'Sensor ID': 'fake_name1', + 'Sensor Reading': 'fake_value1 fake_units', + 'States': '[]', + 'Units': 'fake_units' + }, + 'fake_name2': { + 'Health': '0', + 'Sensor ID': 'fake_name2', + 'Sensor Reading': 'fake_value2 fake_units', + 'States': '[]', + 'Units': 'fake_units' + } + }, + 'fake_type_B': { + 'fake_name3': { + 'Health': '0', + 'Sensor ID': 'fake_name3', + 'Sensor Reading': 'fake_value3 fake_units', + 'States': '[]', 'Units': 'fake_units' + } + } + } + ret = ipminative._get_sensors_data(self.info) + self.assertEqual(expected, ret) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test__get_sensors_data_missing_values(self, ipmi_mock): + reading_1 = self._create_sensor_object('fake_value1', + 'fake_type_A', + 'fake_name1') + reading_2 = self._create_sensor_object(None, + 'fake_type_A', + 'fake_name2') + reading_3 = self._create_sensor_object(None, + 'fake_type_B', + 'fake_name3') + readings = [reading_1, reading_2, reading_3] + ipmicmd = ipmi_mock.return_value + ipmicmd.get_sensor_data.return_value = readings + + expected = { + 'fake_type_A': { + 'fake_name1': { + 'Health': '0', + 'Sensor ID': 'fake_name1', + 'Sensor Reading': 'fake_value1 fake_units', + 'States': '[]', + 'Units': 'fake_units' + } + } + } + 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.""" + + def setUp(self): + super(IPMINativeDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ipminative") + self.driver = driver_factory.get_driver("fake_ipminative") + + self.node = obj_utils.create_test_node(self.context, + driver='fake_ipminative', + driver_info=INFO_DICT) + self.info = ipminative._parse_driver_info(self.node) + + def test_get_properties(self): + 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) + self.assertEqual(sorted(expected), + sorted(self.driver.console.get_properties().keys())) + self.assertEqual(sorted(expected), + sorted(self.driver.get_properties().keys())) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_get_power_state(self, ipmi_mock): + # Getting the mocked command. + cmd_mock = ipmi_mock.return_value + # Getting the get power mock. + get_power_mock = cmd_mock.get_power + + return_values = [{'powerstate': 'error'}, + {'powerstate': 'on'}, + {'powerstate': 'off'}] + + get_power_mock.side_effect = lambda: return_values.pop() + + with task_manager.acquire(self.context, self.node.uuid) as task: + pstate = self.driver.power.get_power_state(task) + self.assertEqual(states.POWER_OFF, pstate) + + pstate = self.driver.power.get_power_state(task) + self.assertEqual(states.POWER_ON, pstate) + + pstate = self.driver.power.get_power_state(task) + self.assertEqual(states.ERROR, pstate) + self.assertEqual(3, get_power_mock.call_count, + "pyghmi.ipmi.command.Command.get_power was not" + " called 3 times.") + + @mock.patch.object(ipminative, '_power_on', autospec=True) + def test_set_power_on_ok(self, power_on_mock): + power_on_mock.return_value = states.POWER_ON + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.power.set_power_state( + task, states.POWER_ON) + power_on_mock.assert_called_once_with(self.info) + + @mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True) + @mock.patch.object(ipminative, '_power_on', autospec=True) + def test_set_power_on_with_next_boot(self, power_on_mock, mock_next_boot): + power_on_mock.return_value = states.POWER_ON + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.power.set_power_state( + task, states.POWER_ON) + mock_next_boot.assert_called_once_with(task, self.info) + power_on_mock.assert_called_once_with(self.info) + + @mock.patch.object(ipminative, '_power_off', autospec=True) + def test_set_power_off_ok(self, power_off_mock): + power_off_mock.return_value = states.POWER_OFF + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.power.set_power_state( + task, states.POWER_OFF) + power_off_mock.assert_called_once_with(self.info) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_set_power_on_fail(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_power.return_value = {'powerstate': 'error'} + + self.config(retry_timeout=500, group='ipmi') + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.PowerStateFailure, + self.driver.power.set_power_state, + task, + states.POWER_ON) + ipmicmd.set_power.assert_called_once_with('on', 500) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_set_boot_device_ok(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_bootdev.return_value = None + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.management.set_boot_device(task, boot_devices.PXE) + # PXE is converted to 'network' internally by ipminative + ipmicmd.set_bootdev.assert_called_once_with('network', persist=False) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_force_set_boot_device_ok(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_bootdev.return_value = None + + with task_manager.acquire(self.context, + self.node.uuid) as task: + task.node.driver_info['ipmi_force_boot_device'] = True + self.driver.management.set_boot_device(task, boot_devices.PXE) + task.node.refresh() + self.assertEqual( + False, + task.node.driver_internal_info['is_next_boot_persistent'] + ) + # PXE is converted to 'network' internally by ipminative + ipmicmd.set_bootdev.assert_called_once_with('network', persist=False) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_set_boot_device_with_persistent(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_bootdev.return_value = None + + with task_manager.acquire(self.context, + self.node.uuid) as task: + task.node.driver_info['ipmi_force_boot_device'] = True + self.driver.management.set_boot_device(task, + boot_devices.PXE, + True) + self.assertEqual( + boot_devices.PXE, + task.node.driver_internal_info['persistent_boot_device']) + # PXE is converted to 'network' internally by ipminative + ipmicmd.set_bootdev.assert_called_once_with('network', persist=False) + + def test_set_boot_device_bad_device(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + self.driver.management.set_boot_device, + task, + 'fake-device') + + @mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True) + @mock.patch.object(ipminative, '_reboot', autospec=True) + def test_reboot_ok(self, reboot_mock, mock_next_boot): + reboot_mock.return_value = None + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.power.reboot(task) + mock_next_boot.assert_called_once_with(task, self.info) + reboot_mock.assert_called_once_with(self.info) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_reboot_fail(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.set_power.return_value = {'powerstate': 'error'} + + self.config(retry_timeout=500, group='ipmi') + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.PowerStateFailure, + self.driver.power.reboot, + task) + ipmicmd.set_power.assert_called_once_with('boot', 500) + + def test_management_interface_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + expected = [boot_devices.PXE, boot_devices.DISK, + boot_devices.CDROM, boot_devices.BIOS] + self.assertEqual(sorted(expected), sorted(task.driver.management. + get_supported_boot_devices(task))) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_management_interface_get_boot_device_good(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_bootdev.return_value = {'bootdev': 'hd'} + with task_manager.acquire(self.context, self.node.uuid) as task: + bootdev = self.driver.management.get_boot_device(task) + self.assertEqual(boot_devices.DISK, bootdev['boot_device']) + self.assertIsNone(bootdev['persistent']) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_management_interface_get_boot_device_persistent(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_bootdev.return_value = {'bootdev': 'hd', + 'persistent': True} + with task_manager.acquire(self.context, self.node.uuid) as task: + bootdev = self.driver.management.get_boot_device(task) + self.assertEqual(boot_devices.DISK, bootdev['boot_device']) + self.assertTrue(bootdev['persistent']) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_management_interface_get_boot_device_fail(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_bootdev.side_effect = pyghmi_exception.IpmiException + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IPMIFailure, + self.driver.management.get_boot_device, task) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_management_interface_get_boot_device_fail_dict(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_bootdev.return_value = {'error': 'boooom'} + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IPMIFailure, + self.driver.management.get_boot_device, task) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_management_interface_get_boot_device_unknown(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_bootdev.return_value = {'bootdev': 'unknown'} + with task_manager.acquire(self.context, self.node.uuid) as task: + expected = {'boot_device': None, 'persistent': None} + self.assertEqual(expected, + self.driver.management.get_boot_device(task)) + + def test_get_force_boot_device_persistent(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['ipmi_force_boot_device'] = True + task.node.driver_internal_info['persistent_boot_device'] = 'pxe' + bootdev = self.driver.management.get_boot_device(task) + self.assertEqual('pxe', bootdev['boot_device']) + self.assertTrue(bootdev['persistent']) + + def test_management_interface_validate_good(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.management.validate(task) + + def test_management_interface_validate_fail(self): + # Missing IPMI driver_info information + node = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + driver='fake_ipminative') + with task_manager.acquire(self.context, node.uuid) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.management.validate, task) + + @mock.patch('pyghmi.ipmi.command.Command', autospec=True) + def test_get_sensors_data(self, ipmi_mock): + ipmicmd = ipmi_mock.return_value + ipmicmd.get_sensor_data.return_value = None + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.management.get_sensors_data(task) + ipmicmd.get_sensor_data.assert_called_once_with() + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console(self, mock_exec): + mock_exec.return_value = None + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.console.start_console(task) + + mock_exec.assert_called_once_with(self.info['uuid'], + self.info['port'], + mock.ANY) + self.assertTrue(mock_exec.called) + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console_fail(self, mock_exec): + mock_exec.side_effect = iter( + [exception.ConsoleSubprocessFailed(error='error')]) + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.ConsoleSubprocessFailed, + self.driver.console.start_console, + task) + + @mock.patch.object(console_utils, 'stop_shellinabox_console', + autospec=True) + def test_stop_console(self, mock_exec): + mock_exec.return_value = None + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.console.stop_console(task) + + mock_exec.assert_called_once_with(self.info['uuid']) + self.assertTrue(mock_exec.called) + + @mock.patch.object(console_utils, 'stop_shellinabox_console', + autospec=True) + def test_stop_console_fail(self, mock_stop): + mock_stop.side_effect = iter([exception.ConsoleError()]) + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.ConsoleError, + self.driver.console.stop_console, + task) + + mock_stop.assert_called_once_with(self.node.uuid) + + @mock.patch.object(console_utils, 'get_shellinabox_console_url', + autospec=True) + def test_get_console(self, mock_exec): + url = 'http://localhost:4201' + mock_exec.return_value = url + expected = {'type': 'shellinabox', 'url': url} + + with task_manager.acquire(self.context, + self.node.uuid) as task: + console_info = self.driver.console.get_console(task) + + 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) diff --git a/ironic/tests/unit/drivers/modules/test_ipmitool.py b/ironic/tests/unit/drivers/modules/test_ipmitool.py new file mode 100644 index 000000000..6a80b7ea9 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_ipmitool.py @@ -0,0 +1,1899 @@ +# coding=utf-8 + +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# Copyright (c) 2012 NTT DOCOMO, INC. +# Copyright 2014 International Business Machines Corporation +# 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. +# + +"""Test class for IPMITool driver module.""" + +import os +import stat +import subprocess +import tempfile +import time +import types + +import mock +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_utils import uuidutils +import six + +from ironic.common import boot_devices +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import states +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.drivers.modules import console_utils +from ironic.drivers.modules import ipmitool as ipmi +from ironic.drivers import utils as driver_utils +from ironic.tests.unit import base +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +CONF = cfg.CONF + +CONF.import_opt('min_command_interval', + 'ironic.drivers.modules.ipminative', + group='ipmi') + +INFO_DICT = db_utils.get_test_ipmi_info() + +# BRIDGE_INFO_DICT will have all the bridging parameters appended +BRIDGE_INFO_DICT = INFO_DICT.copy() +BRIDGE_INFO_DICT.update(db_utils.get_test_ipmi_bridging_parameters()) + + +class IPMIToolCheckInitTestCase(base.TestCase): + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_power_init_calls(self, mock_check_dir, mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = None + ipmi.IPMIPower() + mock_support.assert_called_with(mock.ANY) + mock_check_dir.assert_called_once_with() + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_power_init_calls_raises_1(self, mock_check_dir, mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = None + mock_check_dir.side_effect = iter( + [exception.PathNotFound(dir="foo_dir")]) + self.assertRaises(exception.PathNotFound, ipmi.IPMIPower) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_power_init_calls_raises_2(self, mock_check_dir, mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = None + mock_check_dir.side_effect = iter( + [exception.DirectoryNotWritable(dir="foo_dir")]) + self.assertRaises(exception.DirectoryNotWritable, ipmi.IPMIPower) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_power_init_calls_raises_3(self, mock_check_dir, mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = None + mock_check_dir.side_effect = iter([exception.InsufficientDiskSpace( + path="foo_dir", required=1, actual=0)]) + self.assertRaises(exception.InsufficientDiskSpace, ipmi.IPMIPower) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_power_init_calls_already_checked(self, + mock_check_dir, + mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = True + ipmi.IPMIPower() + mock_support.assert_called_with(mock.ANY) + self.assertEqual(0, mock_check_dir.call_count) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_management_init_calls(self, mock_check_dir, mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = None + + ipmi.IPMIManagement() + mock_support.assert_called_with(mock.ANY) + mock_check_dir.assert_called_once_with() + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_management_init_calls_already_checked(self, + mock_check_dir, + mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = False + + ipmi.IPMIManagement() + mock_support.assert_called_with(mock.ANY) + self.assertEqual(0, mock_check_dir.call_count) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_vendor_passthru_init_calls(self, mock_check_dir, mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = None + ipmi.VendorPassthru() + mock_support.assert_called_with(mock.ANY) + mock_check_dir.assert_called_once_with() + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_vendor_passthru_init_calls_already_checked(self, + mock_check_dir, + mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = True + ipmi.VendorPassthru() + mock_support.assert_called_with(mock.ANY) + self.assertEqual(0, mock_check_dir.call_count) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_console_init_calls(self, mock_check_dir, mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = None + ipmi.IPMIShellinaboxConsole() + mock_support.assert_called_with(mock.ANY) + mock_check_dir.assert_called_once_with() + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'check_dir', autospec=True) + def test_console_init_calls_already_checked(self, + mock_check_dir, + mock_support): + mock_support.return_value = True + ipmi.TMP_DIR_CHECKED = True + ipmi.IPMIShellinaboxConsole() + mock_support.assert_called_with(mock.ANY) + self.assertEqual(0, mock_check_dir.call_count) + + +@mock.patch.object(ipmi, '_is_option_supported', autospec=True) +@mock.patch.object(subprocess, 'check_call', autospec=True) +class IPMIToolCheckOptionSupportedTestCase(base.TestCase): + + def test_check_timing_pass(self, mock_chkcall, mock_support): + mock_chkcall.return_value = (None, None) + mock_support.return_value = None + expected = [mock.call('timing'), + mock.call('timing', True)] + + ipmi._check_option_support(['timing']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_timing_fail(self, mock_chkcall, mock_support): + mock_chkcall.side_effect = iter( + [subprocess.CalledProcessError(1, 'ipmitool')]) + mock_support.return_value = None + expected = [mock.call('timing'), + mock.call('timing', False)] + + ipmi._check_option_support(['timing']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_timing_no_ipmitool(self, mock_chkcall, mock_support): + mock_chkcall.side_effect = iter([OSError()]) + mock_support.return_value = None + expected = [mock.call('timing')] + + self.assertRaises(OSError, ipmi._check_option_support, ['timing']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_single_bridge_pass(self, mock_chkcall, mock_support): + mock_chkcall.return_value = (None, None) + mock_support.return_value = None + expected = [mock.call('single_bridge'), + mock.call('single_bridge', True)] + + ipmi._check_option_support(['single_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_single_bridge_fail(self, mock_chkcall, mock_support): + mock_chkcall.side_effect = iter( + [subprocess.CalledProcessError(1, 'ipmitool')]) + mock_support.return_value = None + expected = [mock.call('single_bridge'), + mock.call('single_bridge', False)] + + ipmi._check_option_support(['single_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_single_bridge_no_ipmitool(self, mock_chkcall, + mock_support): + mock_chkcall.side_effect = iter([OSError()]) + mock_support.return_value = None + expected = [mock.call('single_bridge')] + + self.assertRaises(OSError, ipmi._check_option_support, + ['single_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_dual_bridge_pass(self, mock_chkcall, mock_support): + mock_chkcall.return_value = (None, None) + mock_support.return_value = None + expected = [mock.call('dual_bridge'), + mock.call('dual_bridge', True)] + + ipmi._check_option_support(['dual_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_dual_bridge_fail(self, mock_chkcall, mock_support): + mock_chkcall.side_effect = iter( + [subprocess.CalledProcessError(1, 'ipmitool')]) + mock_support.return_value = None + expected = [mock.call('dual_bridge'), + mock.call('dual_bridge', False)] + + ipmi._check_option_support(['dual_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_dual_bridge_no_ipmitool(self, mock_chkcall, mock_support): + mock_chkcall.side_effect = iter([OSError()]) + mock_support.return_value = None + expected = [mock.call('dual_bridge')] + + self.assertRaises(OSError, ipmi._check_option_support, + ['dual_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_all_options_pass(self, mock_chkcall, mock_support): + mock_chkcall.return_value = (None, None) + mock_support.return_value = None + expected = [ + mock.call('timing'), mock.call('timing', True), + mock.call('single_bridge'), + mock.call('single_bridge', True), + mock.call('dual_bridge'), mock.call('dual_bridge', True)] + + ipmi._check_option_support(['timing', 'single_bridge', 'dual_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_all_options_fail(self, mock_chkcall, mock_support): + options = ['timing', 'single_bridge', 'dual_bridge'] + mock_chkcall.side_effect = iter( + [subprocess.CalledProcessError(1, 'ipmitool')] * len(options)) + mock_support.return_value = None + expected = [ + mock.call('timing'), mock.call('timing', False), + mock.call('single_bridge'), + mock.call('single_bridge', False), + mock.call('dual_bridge'), + mock.call('dual_bridge', False)] + + ipmi._check_option_support(options) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + def test_check_all_options_no_ipmitool(self, mock_chkcall, mock_support): + mock_chkcall.side_effect = iter([OSError()]) + mock_support.return_value = None + # exception is raised once ipmitool was not found for an command + expected = [mock.call('timing')] + + self.assertRaises(OSError, ipmi._check_option_support, + ['timing', 'single_bridge', 'dual_bridge']) + self.assertTrue(mock_chkcall.called) + self.assertEqual(expected, mock_support.call_args_list) + + +@mock.patch.object(time, 'sleep', autospec=True) +class IPMIToolPrivateMethodTestCase(db_base.DbTestCase): + + def setUp(self): + super(IPMIToolPrivateMethodTestCase, self).setUp() + self.node = obj_utils.get_test_node( + self.context, + driver='fake_ipmitool', + driver_info=INFO_DICT) + self.info = ipmi._parse_driver_info(self.node) + + def _test__make_password_file(self, mock_sleep, input_password, + exception_to_raise=None): + pw_file = None + try: + with ipmi._make_password_file(input_password) as pw_file: + if exception_to_raise is not None: + raise exception_to_raise + self.assertTrue(os.path.isfile(pw_file)) + self.assertEqual(0o600, os.stat(pw_file)[stat.ST_MODE] & 0o777) + with open(pw_file, "r") as f: + password = f.read() + self.assertEqual(str(input_password), password) + finally: + if pw_file is not None: + self.assertFalse(os.path.isfile(pw_file)) + + def test__make_password_file_str_password(self, mock_sleep): + self._test__make_password_file(mock_sleep, self.info.get('password')) + + def test__make_password_file_with_numeric_password(self, mock_sleep): + self._test__make_password_file(mock_sleep, 12345) + + def test__make_password_file_caller_exception(self, mock_sleep): + # Test caller raising exception + result = self.assertRaises( + ValueError, + self._test__make_password_file, + mock_sleep, 12345, ValueError('we should fail')) + self.assertEqual('we should fail', six.text_type(result)) + + @mock.patch.object(tempfile, 'NamedTemporaryFile', + new=mock.MagicMock(side_effect=OSError('Test Error'))) + def test__make_password_file_tempfile_known_exception(self, mock_sleep): + # Test OSError exception in _make_password_file for + # tempfile.NamedTemporaryFile + self.assertRaises( + exception.PasswordFileFailedToCreate, + self._test__make_password_file, mock_sleep, 12345) + + @mock.patch.object( + tempfile, 'NamedTemporaryFile', + new=mock.MagicMock(side_effect=OverflowError('Test Error'))) + def test__make_password_file_tempfile_unknown_exception(self, mock_sleep): + # Test exception in _make_password_file for tempfile.NamedTemporaryFile + result = self.assertRaises( + OverflowError, + self._test__make_password_file, mock_sleep, 12345) + self.assertEqual('Test Error', six.text_type(result)) + + def test__make_password_file_write_exception(self, mock_sleep): + # Test exception in _make_password_file for write() + mock_namedtemp = mock.mock_open(mock.MagicMock(name='JLV')) + with mock.patch('tempfile.NamedTemporaryFile', mock_namedtemp): + mock_filehandle = mock_namedtemp.return_value + mock_write = mock_filehandle.write + mock_write.side_effect = OSError('Test 2 Error') + self.assertRaises( + exception.PasswordFileFailedToCreate, + self._test__make_password_file, mock_sleep, 12345) + + def test__parse_driver_info(self, mock_sleep): + # make sure we get back the expected things + _OPTIONS = ['address', 'username', 'password', 'uuid'] + for option in _OPTIONS: + self.assertIsNotNone(self.info.get(option)) + + info = dict(INFO_DICT) + + # test the default value for 'priv_level' + node = obj_utils.get_test_node(self.context, driver_info=info) + ret = ipmi._parse_driver_info(node) + self.assertEqual('ADMINISTRATOR', ret['priv_level']) + + # ipmi_username / ipmi_password are not mandatory + del info['ipmi_username'] + node = obj_utils.get_test_node(self.context, driver_info=info) + ipmi._parse_driver_info(node) + del info['ipmi_password'] + node = obj_utils.get_test_node(self.context, driver_info=info) + ipmi._parse_driver_info(node) + + # make sure error is raised when ipmi_address is missing + info = dict(INFO_DICT) + del info['ipmi_address'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + ipmi._parse_driver_info, + node) + + # test the invalid priv_level value + info = dict(INFO_DICT) + info['ipmi_priv_level'] = 'ABCD' + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.InvalidParameterValue, + ipmi._parse_driver_info, + node) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_invalid_bridging_type( + self, mock_support, mock_sleep): + info = BRIDGE_INFO_DICT.copy() + # make sure error is raised when ipmi_bridging has unexpected value + info['ipmi_bridging'] = 'junk' + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.InvalidParameterValue, + ipmi._parse_driver_info, + node) + self.assertFalse(mock_support.called) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_no_bridging( + self, mock_support, mock_sleep): + _OPTIONS = ['address', 'username', 'password', 'uuid'] + _BRIDGING_OPTIONS = ['local_address', 'transit_channel', + 'transit_address', + 'target_channel', 'target_address'] + info = BRIDGE_INFO_DICT.copy() + info['ipmi_bridging'] = 'no' + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=info) + ret = ipmi._parse_driver_info(node) + + # ensure that _is_option_supported was not called + self.assertFalse(mock_support.called) + # check if we got all the required options + for option in _OPTIONS: + self.assertIsNotNone(ret[option]) + # test the default value for 'priv_level' + self.assertEqual('ADMINISTRATOR', ret['priv_level']) + + # check if bridging parameters were set to None + for option in _BRIDGING_OPTIONS: + self.assertIsNone(ret[option]) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_dual_bridging_pass( + self, mock_support, mock_sleep): + _OPTIONS = ['address', 'username', 'password', 'uuid', + 'local_address', 'transit_channel', 'transit_address', + 'target_channel', 'target_address'] + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=BRIDGE_INFO_DICT) + + expected = [mock.call('dual_bridge')] + + # test double bridging and make sure we get back expected result + mock_support.return_value = True + ret = ipmi._parse_driver_info(node) + self.assertEqual(expected, mock_support.call_args_list) + for option in _OPTIONS: + self.assertIsNotNone(ret[option]) + # test the default value for 'priv_level' + self.assertEqual('ADMINISTRATOR', ret['priv_level']) + + info = BRIDGE_INFO_DICT.copy() + # ipmi_local_address / ipmi_username / ipmi_password are not mandatory + for optional_arg in ['ipmi_local_address', 'ipmi_username', + 'ipmi_password']: + del info[optional_arg] + node = obj_utils.get_test_node(self.context, driver_info=info) + ipmi._parse_driver_info(node) + self.assertEqual(mock.call('dual_bridge'), mock_support.call_args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_dual_bridging_not_supported( + self, mock_support, mock_sleep): + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=BRIDGE_INFO_DICT) + # if dual bridge is not supported then check if error is raised + mock_support.return_value = False + self.assertRaises(exception.InvalidParameterValue, + ipmi._parse_driver_info, node) + mock_support.assert_called_once_with('dual_bridge') + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_dual_bridging_missing_parameters( + self, mock_support, mock_sleep): + info = BRIDGE_INFO_DICT.copy() + mock_support.return_value = True + # make sure error is raised when dual bridging is selected and the + # required parameters for dual bridging are not provided + for param in ['ipmi_transit_channel', 'ipmi_target_address', + 'ipmi_transit_address', 'ipmi_target_channel']: + del info[param] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + ipmi._parse_driver_info, node) + self.assertEqual(mock.call('dual_bridge'), + mock_support.call_args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_single_bridging_pass( + self, mock_support, mock_sleep): + _OPTIONS = ['address', 'username', 'password', 'uuid', + 'local_address', 'target_channel', 'target_address'] + + info = BRIDGE_INFO_DICT.copy() + info['ipmi_bridging'] = 'single' + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=info) + + expected = [mock.call('single_bridge')] + + # test single bridging and make sure we get back expected things + mock_support.return_value = True + ret = ipmi._parse_driver_info(node) + self.assertEqual(expected, mock_support.call_args_list) + for option in _OPTIONS: + self.assertIsNotNone(ret[option]) + # test the default value for 'priv_level' + self.assertEqual('ADMINISTRATOR', ret['priv_level']) + + # check if dual bridge params are set to None + self.assertIsNone(ret['transit_channel']) + self.assertIsNone(ret['transit_address']) + + # ipmi_local_address / ipmi_username / ipmi_password are not mandatory + for optional_arg in ['ipmi_local_address', 'ipmi_username', + 'ipmi_password']: + del info[optional_arg] + node = obj_utils.get_test_node(self.context, driver_info=info) + ipmi._parse_driver_info(node) + self.assertEqual(mock.call('single_bridge'), + mock_support.call_args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_single_bridging_not_supported( + self, mock_support, mock_sleep): + info = BRIDGE_INFO_DICT.copy() + info['ipmi_bridging'] = 'single' + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=info) + + # if single bridge is not supported then check if error is raised + mock_support.return_value = False + self.assertRaises(exception.InvalidParameterValue, + ipmi._parse_driver_info, node) + mock_support.assert_called_once_with('single_bridge') + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + def test__parse_driver_info_with_single_bridging_missing_parameters( + self, mock_support, mock_sleep): + info = dict(BRIDGE_INFO_DICT) + info['ipmi_bridging'] = 'single' + mock_support.return_value = True + # make sure error is raised when single bridging is selected and the + # required parameters for single bridging are not provided + for param in ['ipmi_target_channel', 'ipmi_target_address']: + del info[param] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + ipmi._parse_driver_info, + node) + self.assertEqual(mock.call('single_bridge'), + mock_support.call_args) + + def test__parse_driver_info_ipmi_prot_version_1_5(self, mock_sleep): + info = dict(INFO_DICT) + info['ipmi_protocol_version'] = '1.5' + node = obj_utils.get_test_node(self.context, driver_info=info) + ret = ipmi._parse_driver_info(node) + self.assertEqual('1.5', ret['protocol_version']) + + def test__parse_driver_info_invalid_ipmi_prot_version(self, mock_sleep): + info = dict(INFO_DICT) + info['ipmi_protocol_version'] = '9000' + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.InvalidParameterValue, + ipmi._parse_driver_info, node) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_first_call_to_address(self, mock_exec, mock_pwf, + mock_support, mock_sleep): + ipmi.LAST_CMD_TIME = {} + pw_file_handle = tempfile.NamedTemporaryFile() + pw_file = pw_file_handle.name + file_handle = open(pw_file, "w") + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle, + 'A', 'B', 'C', + ] + + mock_support.return_value = False + mock_pwf.return_value = file_handle + mock_exec.return_value = (None, None) + + ipmi._exec_ipmitool(self.info, 'A B C') + + mock_support.assert_called_once_with('timing') + mock_pwf.assert_called_once_with(self.info['password']) + mock_exec.assert_called_once_with(*args) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_second_call_to_address_sleep( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + ipmi.LAST_CMD_TIME = {} + pw_file_handle1 = tempfile.NamedTemporaryFile() + pw_file1 = pw_file_handle1.name + file_handle1 = open(pw_file1, "w") + pw_file_handle2 = tempfile.NamedTemporaryFile() + pw_file2 = pw_file_handle2.name + file_handle2 = open(pw_file2, "w") + args = [[ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle1, + 'A', 'B', 'C', + ], [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle2, + 'D', 'E', 'F', + ]] + + expected = [mock.call('timing'), + mock.call('timing')] + mock_support.return_value = False + mock_pwf.side_effect = iter([file_handle1, file_handle2]) + mock_exec.side_effect = iter([(None, None), (None, None)]) + + ipmi._exec_ipmitool(self.info, 'A B C') + mock_exec.assert_called_with(*args[0]) + + ipmi._exec_ipmitool(self.info, 'D E F') + self.assertTrue(mock_sleep.called) + self.assertEqual(expected, mock_support.call_args_list) + mock_exec.assert_called_with(*args[1]) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_second_call_to_address_no_sleep( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + ipmi.LAST_CMD_TIME = {} + pw_file_handle1 = tempfile.NamedTemporaryFile() + pw_file1 = pw_file_handle1.name + file_handle1 = open(pw_file1, "w") + pw_file_handle2 = tempfile.NamedTemporaryFile() + pw_file2 = pw_file_handle2.name + file_handle2 = open(pw_file2, "w") + args = [[ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle1, + 'A', 'B', 'C', + ], [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle2, + 'D', 'E', 'F', + ]] + + expected = [mock.call('timing'), + mock.call('timing')] + mock_support.return_value = False + mock_pwf.side_effect = iter([file_handle1, file_handle2]) + mock_exec.side_effect = iter([(None, None), (None, None)]) + + ipmi._exec_ipmitool(self.info, 'A B C') + mock_exec.assert_called_with(*args[0]) + # act like enough time has passed + ipmi.LAST_CMD_TIME[self.info['address']] = ( + time.time() - CONF.ipmi.min_command_interval) + ipmi._exec_ipmitool(self.info, 'D E F') + self.assertFalse(mock_sleep.called) + self.assertEqual(expected, mock_support.call_args_list) + mock_exec.assert_called_with(*args[1]) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_two_calls_to_diff_address( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + ipmi.LAST_CMD_TIME = {} + pw_file_handle1 = tempfile.NamedTemporaryFile() + pw_file1 = pw_file_handle1.name + file_handle1 = open(pw_file1, "w") + pw_file_handle2 = tempfile.NamedTemporaryFile() + pw_file2 = pw_file_handle2.name + file_handle2 = open(pw_file2, "w") + args = [[ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle1, + 'A', 'B', 'C', + ], [ + 'ipmitool', + '-I', 'lanplus', + '-H', '127.127.127.127', + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle2, + 'D', 'E', 'F', + ]] + + expected = [mock.call('timing'), + mock.call('timing')] + mock_support.return_value = False + mock_pwf.side_effect = iter([file_handle1, file_handle2]) + mock_exec.side_effect = iter([(None, None), (None, None)]) + + ipmi._exec_ipmitool(self.info, 'A B C') + mock_exec.assert_called_with(*args[0]) + self.info['address'] = '127.127.127.127' + ipmi._exec_ipmitool(self.info, 'D E F') + self.assertFalse(mock_sleep.called) + self.assertEqual(expected, mock_support.call_args_list) + mock_exec.assert_called_with(*args[1]) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_without_timing( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + pw_file_handle = tempfile.NamedTemporaryFile() + pw_file = pw_file_handle.name + file_handle = open(pw_file, "w") + + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle, + 'A', 'B', 'C', + ] + + mock_support.return_value = False + mock_pwf.return_value = file_handle + mock_exec.return_value = (None, None) + + ipmi._exec_ipmitool(self.info, 'A B C') + + mock_support.assert_called_once_with('timing') + mock_pwf.assert_called_once_with(self.info['password']) + mock_exec.assert_called_once_with(*args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_with_timing( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + pw_file_handle = tempfile.NamedTemporaryFile() + pw_file = pw_file_handle.name + file_handle = open(pw_file, "w") + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-R', '12', + '-N', '5', + '-f', file_handle, + 'A', 'B', 'C', + ] + + mock_support.return_value = True + mock_pwf.return_value = file_handle + mock_exec.return_value = (None, None) + + ipmi._exec_ipmitool(self.info, 'A B C') + + mock_support.assert_called_once_with('timing') + mock_pwf.assert_called_once_with(self.info['password']) + mock_exec.assert_called_once_with(*args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_without_username( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + self.info['username'] = None + pw_file_handle = tempfile.NamedTemporaryFile() + pw_file = pw_file_handle.name + file_handle = open(pw_file, "w") + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-f', file_handle, + 'A', 'B', 'C', + ] + + mock_support.return_value = False + mock_pwf.return_value = file_handle + mock_exec.return_value = (None, None) + ipmi._exec_ipmitool(self.info, 'A B C') + mock_support.assert_called_once_with('timing') + self.assertTrue(mock_pwf.called) + mock_exec.assert_called_once_with(*args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_with_dual_bridging(self, + mock_exec, mock_pwf, + mock_support, + mock_sleep): + + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=BRIDGE_INFO_DICT) + # when support for dual bridge command is called returns True + mock_support.return_value = True + info = ipmi._parse_driver_info(node) + pw_file_handle = tempfile.NamedTemporaryFile() + pw_file = pw_file_handle.name + file_handle = open(pw_file, "w") + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', info['address'], + '-L', info['priv_level'], + '-U', info['username'], + '-m', info['local_address'], + '-B', info['transit_channel'], + '-T', info['transit_address'], + '-b', info['target_channel'], + '-t', info['target_address'], + '-f', file_handle, + 'A', 'B', 'C', + ] + + expected = [mock.call('dual_bridge'), + mock.call('timing')] + # When support for timing command is called returns False + mock_support.return_value = False + mock_pwf.return_value = file_handle + mock_exec.return_value = (None, None) + ipmi._exec_ipmitool(info, 'A B C') + self.assertEqual(expected, mock_support.call_args_list) + self.assertTrue(mock_pwf.called) + mock_exec.assert_called_once_with(*args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_with_single_bridging(self, + mock_exec, mock_pwf, + mock_support, + mock_sleep): + single_bridge_info = dict(BRIDGE_INFO_DICT) + single_bridge_info['ipmi_bridging'] = 'single' + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=single_bridge_info) + # when support for single bridge command is called returns True + mock_support.return_value = True + info = ipmi._parse_driver_info(node) + info['transit_channel'] = info['transit_address'] = None + + pw_file_handle = tempfile.NamedTemporaryFile() + pw_file = pw_file_handle.name + file_handle = open(pw_file, "w") + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', info['address'], + '-L', info['priv_level'], + '-U', info['username'], + '-m', info['local_address'], + '-b', info['target_channel'], + '-t', info['target_address'], + '-f', file_handle, + 'A', 'B', 'C', + ] + + expected = [mock.call('single_bridge'), + mock.call('timing')] + # When support for timing command is called returns False + mock_support.return_value = False + mock_pwf.return_value = file_handle + mock_exec.return_value = (None, None) + ipmi._exec_ipmitool(info, 'A B C') + self.assertEqual(expected, mock_support.call_args_list) + self.assertTrue(mock_pwf.called) + mock_exec.assert_called_once_with(*args) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_exception( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + pw_file_handle = tempfile.NamedTemporaryFile() + pw_file = pw_file_handle.name + file_handle = open(pw_file, "w") + args = [ + 'ipmitool', + '-I', 'lanplus', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', file_handle, + 'A', 'B', 'C', + ] + + mock_support.return_value = False + mock_pwf.return_value = file_handle + mock_exec.side_effect = iter([processutils.ProcessExecutionError("x")]) + self.assertRaises(processutils.ProcessExecutionError, + ipmi._exec_ipmitool, + self.info, 'A B C') + mock_support.assert_called_once_with('timing') + mock_pwf.assert_called_once_with(self.info['password']) + mock_exec.assert_called_once_with(*args) + self.assertEqual(1, mock_exec.call_count) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_exception_retry( + self, mock_exec, mock_support, mock_sleep): + + ipmi.LAST_CMD_TIME = {} + mock_support.return_value = False + mock_exec.side_effect = iter([ + processutils.ProcessExecutionError( + stderr="insufficient resources for session" + ), + (None, None) + ]) + + # Directly set the configuration values such that + # the logic will cause _exec_ipmitool to retry twice. + self.config(min_command_interval=1, group='ipmi') + self.config(retry_timeout=2, group='ipmi') + + ipmi._exec_ipmitool(self.info, 'A B C') + + mock_support.assert_called_once_with('timing') + self.assertEqual(2, mock_exec.call_count) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_exception_retries_exceeded( + self, mock_exec, mock_support, mock_sleep): + + ipmi.LAST_CMD_TIME = {} + mock_support.return_value = False + + mock_exec.side_effect = iter([processutils.ProcessExecutionError( + stderr="insufficient resources for session" + )]) + + # Directly set the configuration values such that + # the logic will cause _exec_ipmitool to timeout. + self.config(min_command_interval=1, group='ipmi') + self.config(retry_timeout=1, group='ipmi') + + self.assertRaises(processutils.ProcessExecutionError, + ipmi._exec_ipmitool, + self.info, 'A B C') + mock_support.assert_called_once_with('timing') + self.assertEqual(1, mock_exec.call_count) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_exception_non_retryable_failure( + self, mock_exec, mock_support, mock_sleep): + + ipmi.LAST_CMD_TIME = {} + mock_support.return_value = False + + # Return a retryable error, then an error that cannot + # be retried thus resulting in a single retry + # attempt by _exec_ipmitool. + mock_exec.side_effect = iter([ + processutils.ProcessExecutionError( + stderr="insufficient resources for session" + ), + processutils.ProcessExecutionError( + stderr="Unknown" + ), + ]) + + # Directly set the configuration values such that + # the logic will cause _exec_ipmitool to retry up + # to 3 times. + self.config(min_command_interval=1, group='ipmi') + self.config(retry_timeout=3, group='ipmi') + + self.assertRaises(processutils.ProcessExecutionError, + ipmi._exec_ipmitool, + self.info, 'A B C') + mock_support.assert_called_once_with('timing') + self.assertEqual(2, mock_exec.call_count) + + @mock.patch.object(ipmi, '_is_option_supported', autospec=True) + @mock.patch.object(ipmi, '_make_password_file', autospec=True) + @mock.patch.object(utils, 'execute', autospec=True) + def test__exec_ipmitool_IPMI_version_1_5( + self, mock_exec, mock_pwf, mock_support, mock_sleep): + self.info['protocol_version'] = '1.5' + # Assert it uses "-I lan" (1.5) instead of "-I lanplus" (2.0) + args = [ + 'ipmitool', + '-I', 'lan', + '-H', self.info['address'], + '-L', self.info['priv_level'], + '-U', self.info['username'], + '-f', mock.ANY, + 'A', 'B', 'C', + ] + + mock_support.return_value = False + mock_exec.return_value = (None, None) + ipmi._exec_ipmitool(self.info, 'A B C') + mock_support.assert_called_once_with('timing') + self.assertTrue(mock_pwf.called) + mock_exec.assert_called_once_with(*args) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test__power_status_on(self, mock_exec, mock_sleep): + mock_exec.return_value = ["Chassis Power is on\n", None] + + state = ipmi._power_status(self.info) + + mock_exec.assert_called_once_with(self.info, "power status") + self.assertEqual(states.POWER_ON, state) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test__power_status_off(self, mock_exec, mock_sleep): + mock_exec.return_value = ["Chassis Power is off\n", None] + + state = ipmi._power_status(self.info) + + mock_exec.assert_called_once_with(self.info, "power status") + self.assertEqual(states.POWER_OFF, state) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test__power_status_error(self, mock_exec, mock_sleep): + mock_exec.return_value = ["Chassis Power is badstate\n", None] + + state = ipmi._power_status(self.info) + + mock_exec.assert_called_once_with(self.info, "power status") + self.assertEqual(states.ERROR, state) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test__power_status_exception(self, mock_exec, mock_sleep): + mock_exec.side_effect = iter( + [processutils.ProcessExecutionError("error")]) + self.assertRaises(exception.IPMIFailure, + ipmi._power_status, + self.info) + mock_exec.assert_called_once_with(self.info, "power status") + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + @mock.patch('eventlet.greenthread.sleep', autospec=True) + def test__power_on_max_retries(self, sleep_mock, mock_exec, mock_sleep): + self.config(retry_timeout=2, group='ipmi') + + def side_effect(driver_info, command): + resp_dict = {"power status": ["Chassis Power is off\n", None], + "power on": [None, None]} + return resp_dict.get(command, ["Bad\n", None]) + + mock_exec.side_effect = side_effect + + expected = [mock.call(self.info, "power on"), + mock.call(self.info, "power status"), + mock.call(self.info, "power status")] + + state = ipmi._power_on(self.info) + + self.assertEqual(mock_exec.call_args_list, expected) + self.assertEqual(states.ERROR, state) + + +class IPMIToolDriverTestCase(db_base.DbTestCase): + + def setUp(self): + super(IPMIToolDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ipmitool") + self.driver = driver_factory.get_driver("fake_ipmitool") + + self.node = obj_utils.create_test_node(self.context, + driver='fake_ipmitool', + driver_info=INFO_DICT) + self.info = ipmi._parse_driver_info(self.node) + + @mock.patch.object(ipmi, "_parse_driver_info", autospec=True) + def test_power_validate(self, mock_parse): + node = obj_utils.get_test_node(self.context, driver='fake_ipmitool', + driver_info=INFO_DICT) + mock_parse.return_value = {} + + with task_manager.acquire(self.context, node.uuid) as task: + task.driver.power.validate(task) + mock_parse.assert_called_once_with(mock.ANY) + + def test_get_properties(self): + expected = ipmi.COMMON_PROPERTIES + self.assertEqual(expected, self.driver.power.get_properties()) + + expected = list(ipmi.COMMON_PROPERTIES) + list(ipmi.CONSOLE_PROPERTIES) + self.assertEqual(sorted(expected), + sorted(self.driver.console.get_properties().keys())) + self.assertEqual(sorted(expected), + sorted(self.driver.get_properties().keys())) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_get_power_state(self, mock_exec): + returns = iter([["Chassis Power is off\n", None], + ["Chassis Power is on\n", None], + ["\n", None]]) + expected = [mock.call(self.info, "power status"), + mock.call(self.info, "power status"), + mock.call(self.info, "power status")] + mock_exec.side_effect = returns + + with task_manager.acquire(self.context, self.node.uuid) as task: + pstate = self.driver.power.get_power_state(task) + self.assertEqual(states.POWER_OFF, pstate) + + pstate = self.driver.power.get_power_state(task) + self.assertEqual(states.POWER_ON, pstate) + + pstate = self.driver.power.get_power_state(task) + self.assertEqual(states.ERROR, pstate) + + self.assertEqual(mock_exec.call_args_list, expected) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_get_power_state_exception(self, mock_exec): + mock_exec.side_effect = iter( + [processutils.ProcessExecutionError("error")]) + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IPMIFailure, + self.driver.power.get_power_state, + task) + mock_exec.assert_called_once_with(self.info, "power status") + + @mock.patch.object(ipmi, '_power_on', autospec=True) + @mock.patch.object(ipmi, '_power_off', autospec=True) + def test_set_power_on_ok(self, mock_off, mock_on): + self.config(retry_timeout=0, group='ipmi') + + mock_on.return_value = states.POWER_ON + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.power.set_power_state(task, + states.POWER_ON) + + mock_on.assert_called_once_with(self.info) + self.assertFalse(mock_off.called) + + @mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True) + @mock.patch.object(ipmi, '_power_on', autospec=True) + @mock.patch.object(ipmi, '_power_off', autospec=True) + def test_set_power_on_with_next_boot(self, mock_off, mock_on, + mock_next_boot): + self.config(retry_timeout=0, group='ipmi') + + mock_on.return_value = states.POWER_ON + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.power.set_power_state(task, + states.POWER_ON) + mock_next_boot.assert_called_once_with(task, self.info) + + mock_on.assert_called_once_with(self.info) + self.assertFalse(mock_off.called) + + @mock.patch.object(ipmi, '_power_on', autospec=True) + @mock.patch.object(ipmi, '_power_off', autospec=True) + def test_set_power_off_ok(self, mock_off, mock_on): + self.config(retry_timeout=0, group='ipmi') + + mock_off.return_value = states.POWER_OFF + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.power.set_power_state(task, + states.POWER_OFF) + + mock_off.assert_called_once_with(self.info) + self.assertFalse(mock_on.called) + + @mock.patch.object(ipmi, '_power_on', autospec=True) + @mock.patch.object(ipmi, '_power_off', autospec=True) + def test_set_power_on_fail(self, mock_off, mock_on): + self.config(retry_timeout=0, group='ipmi') + + mock_on.return_value = states.ERROR + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.assertRaises(exception.PowerStateFailure, + self.driver.power.set_power_state, + task, + states.POWER_ON) + + mock_on.assert_called_once_with(self.info) + self.assertFalse(mock_off.called) + + def test_set_power_invalid_state(self): + with task_manager.acquire(self.context, self.node['uuid']) as task: + self.assertRaises(exception.InvalidParameterValue, + self.driver.power.set_power_state, + task, + "fake state") + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_send_raw_bytes_ok(self, mock_exec): + mock_exec.return_value = [None, None] + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.vendor.send_raw(task, http_method='POST', + raw_bytes='0x00 0x01') + + mock_exec.assert_called_once_with(self.info, 'raw 0x00 0x01') + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_send_raw_bytes_fail(self, mock_exec): + mock_exec.side_effect = iter( + [exception.PasswordFileFailedToCreate('error')]) + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.assertRaises(exception.IPMIFailure, + self.driver.vendor.send_raw, + task, + http_method='POST', + raw_bytes='0x00 0x01') + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test__bmc_reset_ok(self, mock_exec): + mock_exec.return_value = [None, None] + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.vendor.bmc_reset(task, 'POST') + + mock_exec.assert_called_once_with(self.info, 'bmc reset warm') + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test__bmc_reset_cold(self, mock_exec): + mock_exec.return_value = [None, None] + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.vendor.bmc_reset(task, 'POST', warm=False) + + mock_exec.assert_called_once_with(self.info, 'bmc reset cold') + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test__bmc_reset_fail(self, mock_exec): + mock_exec.side_effect = iter([processutils.ProcessExecutionError()]) + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.assertRaises(exception.IPMIFailure, + self.driver.vendor.bmc_reset, + task, 'POST') + + @mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True) + @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + def test_reboot_ok(self, mock_on, mock_off, mock_next_boot): + manager = mock.MagicMock() + # NOTE(rloo): if autospec is True, then manager.mock_calls is empty + mock_on.return_value = states.POWER_ON + manager.attach_mock(mock_off, 'power_off') + manager.attach_mock(mock_on, 'power_on') + expected = [mock.call.power_off(self.info), + mock.call.power_on(self.info)] + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.power.reboot(task) + mock_next_boot.assert_called_once_with(task, self.info) + + self.assertEqual(manager.mock_calls, expected) + + @mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType) + @mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType) + def test_reboot_fail(self, mock_on, mock_off): + manager = mock.MagicMock() + # NOTE(rloo): if autospec is True, then manager.mock_calls is empty + mock_on.return_value = states.ERROR + manager.attach_mock(mock_off, 'power_off') + manager.attach_mock(mock_on, 'power_on') + expected = [mock.call.power_off(self.info), + mock.call.power_on(self.info)] + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.assertRaises(exception.PowerStateFailure, + self.driver.power.reboot, + task) + + self.assertEqual(manager.mock_calls, expected) + + @mock.patch.object(ipmi, '_parse_driver_info', autospec=True) + def test_vendor_passthru_validate__parse_driver_info_fail(self, info_mock): + info_mock.side_effect = iter([exception.InvalidParameterValue("bad")]) + with task_manager.acquire(self.context, self.node['uuid']) as task: + self.assertRaises(exception.InvalidParameterValue, + self.driver.vendor.validate, + task, method='send_raw', raw_bytes='0x00 0x01') + info_mock.assert_called_once_with(task.node) + + def test_vendor_passthru_validate__send_raw_bytes_good(self): + 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') + + 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') + + @mock.patch.object(ipmi.VendorPassthru, 'send_raw', autospec=True) + def test_vendor_passthru_call_send_raw_bytes(self, raw_bytes_mock): + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.driver.vendor.send_raw(task, http_method='POST', + raw_bytes='0x00 0x01') + raw_bytes_mock.assert_called_once_with( + self.driver.vendor, task, http_method='POST', + raw_bytes='0x00 0x01') + + def test_vendor_passthru_validate__bmc_reset_good(self): + with task_manager.acquire(self.context, self.node['uuid']) as task: + self.driver.vendor.validate(task, + method='bmc_reset') + + def test_vendor_passthru_validate__bmc_reset_warm_good(self): + with task_manager.acquire(self.context, self.node['uuid']) as task: + self.driver.vendor.validate(task, + method='bmc_reset', + warm=True) + + def test_vendor_passthru_validate__bmc_reset_cold_good(self): + with task_manager.acquire(self.context, self.node['uuid']) as task: + self.driver.vendor.validate(task, + method='bmc_reset', + warm=False) + + @mock.patch.object(ipmi.VendorPassthru, 'bmc_reset', autospec=True) + def test_vendor_passthru_call_bmc_reset_warm(self, bmc_mock): + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.driver.vendor.bmc_reset(task, 'POST', warm=True) + bmc_mock.assert_called_once_with( + self.driver.vendor, task, 'POST', warm=True) + + @mock.patch.object(ipmi.VendorPassthru, 'bmc_reset', autospec=True) + def test_vendor_passthru_call_bmc_reset_cold(self, bmc_mock): + with task_manager.acquire(self.context, self.node['uuid'], + shared=False) as task: + self.driver.vendor.bmc_reset(task, 'POST', warm=False) + bmc_mock.assert_called_once_with( + self.driver.vendor, task, 'POST', warm=False) + + 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)) + + def test_vendor_passthru_driver_routes(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + driver_routes = task.driver.vendor.driver_routes + self.assertIsInstance(driver_routes, dict) + self.assertEqual({}, driver_routes) + + def test_console_validate(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.driver_info['ipmi_terminal_port'] = 123 + task.driver.console.validate(task) + + def test_console_validate_missing_port(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.driver_info.pop('ipmi_terminal_port', None) + self.assertRaises(exception.MissingParameterValue, + task.driver.console.validate, task) + + def test_console_validate_invalid_port(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.driver_info['ipmi_terminal_port'] = '' + self.assertRaises(exception.InvalidParameterValue, + task.driver.console.validate, task) + + def test_console_validate_wrong_ipmi_protocol_version(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.driver_info['ipmi_terminal_port'] = 123 + task.node.driver_info['ipmi_protocol_version'] = '1.5' + self.assertRaises(exception.InvalidParameterValue, + task.driver.console.validate, task) + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console(self, mock_exec): + mock_exec.return_value = None + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.console.start_console(task) + + mock_exec.assert_called_once_with(self.info['uuid'], + self.info['port'], + mock.ANY) + self.assertTrue(mock_exec.called) + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console_fail(self, mock_exec): + mock_exec.side_effect = iter( + [exception.ConsoleSubprocessFailed(error='error')]) + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.assertRaises(exception.ConsoleSubprocessFailed, + self.driver.console.start_console, + task) + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console_fail_nodir(self, mock_exec): + mock_exec.side_effect = iter([exception.ConsoleError()]) + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.ConsoleError, + self.driver.console.start_console, + task) + mock_exec.assert_called_once_with(self.node.uuid, mock.ANY, mock.ANY) + + @mock.patch.object(console_utils, 'stop_shellinabox_console', + autospec=True) + def test_stop_console(self, mock_exec): + mock_exec.return_value = None + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + self.driver.console.stop_console(task) + + mock_exec.assert_called_once_with(self.info['uuid']) + self.assertTrue(mock_exec.called) + + @mock.patch.object(console_utils, 'stop_shellinabox_console', + autospec=True) + def test_stop_console_fail(self, mock_stop): + mock_stop.side_effect = iter([exception.ConsoleError()]) + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.ConsoleError, + self.driver.console.stop_console, + task) + + mock_stop.assert_called_once_with(self.node.uuid) + + @mock.patch.object(console_utils, 'get_shellinabox_console_url', + autospec=True) + def test_get_console(self, mock_exec): + url = 'http://localhost:4201' + mock_exec.return_value = url + expected = {'type': 'shellinabox', 'url': url} + + with task_manager.acquire(self.context, + self.node['uuid']) as task: + console_info = self.driver.console.get_console(task) + + self.assertEqual(expected, console_info) + mock_exec.assert_called_once_with(self.info['port']) + self.assertTrue(mock_exec.called) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_set_boot_device_ok(self, mock_exec): + mock_exec.return_value = [None, None] + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.driver.management.set_boot_device(task, boot_devices.PXE) + + mock_calls = [mock.call(self.info, "raw 0x00 0x08 0x03 0x08"), + mock.call(self.info, "chassis bootdev pxe")] + mock_exec.assert_has_calls(mock_calls) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_force_set_boot_device_ok(self, mock_exec): + mock_exec.return_value = [None, None] + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['ipmi_force_boot_device'] = True + self.info['force_boot_device'] = True + self.driver.management.set_boot_device(task, boot_devices.PXE) + task.node.refresh() + self.assertEqual( + False, + task.node.driver_internal_info['is_next_boot_persistent'] + ) + + mock_calls = [mock.call(self.info, "raw 0x00 0x08 0x03 0x08"), + mock.call(self.info, "chassis bootdev pxe")] + mock_exec.assert_has_calls(mock_calls) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_set_boot_device_persistent(self, mock_exec): + mock_exec.return_value = [None, None] + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['ipmi_force_boot_device'] = True + self.info['force_boot_device'] = True + self.driver.management.set_boot_device(task, + boot_devices.PXE, + True) + self.assertEqual( + boot_devices.PXE, + task.node.driver_internal_info['persistent_boot_device']) + + mock_calls = [mock.call(self.info, "raw 0x00 0x08 0x03 0x08"), + mock.call(self.info, "chassis bootdev pxe")] + mock_exec.assert_has_calls(mock_calls) + + def test_management_interface_set_boot_device_bad_device(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + self.driver.management.set_boot_device, + task, 'fake-device') + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_set_boot_device_exec_failed(self, mock_exec): + mock_exec.side_effect = iter([processutils.ProcessExecutionError()]) + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IPMIFailure, + self.driver.management.set_boot_device, + task, boot_devices.PXE) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_set_boot_device_unknown_exception(self, + mock_exec): + + class FakeException(Exception): + pass + + mock_exec.side_effect = iter([FakeException('boom')]) + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(FakeException, + self.driver.management.set_boot_device, + task, boot_devices.PXE) + + def test_management_interface_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + expected = [boot_devices.PXE, boot_devices.DISK, + boot_devices.CDROM, boot_devices.BIOS, + boot_devices.SAFE] + self.assertEqual(sorted(expected), sorted(task.driver.management. + get_supported_boot_devices(task))) + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_get_boot_device(self, mock_exec): + # output, expected boot device + bootdevs = [('Boot Device Selector : ' + 'Force Boot from default Hard-Drive\n', + boot_devices.DISK), + ('Boot Device Selector : ' + 'Force Boot from default Hard-Drive, request Safe-Mode\n', + boot_devices.SAFE), + ('Boot Device Selector : ' + 'Force Boot into BIOS Setup\n', + boot_devices.BIOS), + ('Boot Device Selector : ' + 'Force PXE\n', + boot_devices.PXE), + ('Boot Device Selector : ' + 'Force Boot from CD/DVD\n', + boot_devices.CDROM)] + with task_manager.acquire(self.context, self.node.uuid) as task: + for out, expected_device in bootdevs: + mock_exec.return_value = (out, '') + expected_response = {'boot_device': expected_device, + 'persistent': False} + self.assertEqual(expected_response, + task.driver.management.get_boot_device(task)) + mock_exec.assert_called_with(mock.ANY, + "chassis bootparam get 5") + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_get_boot_device_unknown_dev(self, mock_exec): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_exec.return_value = ('Boot Device Selector : Fake\n', '') + response = task.driver.management.get_boot_device(task) + self.assertIsNone(response['boot_device']) + mock_exec.assert_called_with(mock.ANY, "chassis bootparam get 5") + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_get_boot_device_fail(self, mock_exec): + with task_manager.acquire(self.context, self.node.uuid) as task: + mock_exec.side_effect = iter( + [processutils.ProcessExecutionError()]) + self.assertRaises(exception.IPMIFailure, + task.driver.management.get_boot_device, task) + mock_exec.assert_called_with(mock.ANY, "chassis bootparam get 5") + + @mock.patch.object(ipmi, '_exec_ipmitool', autospec=True) + def test_management_interface_get_boot_device_persistent(self, mock_exec): + outputs = [('Options apply to only next boot\n' + 'Boot Device Selector : Force PXE\n', + False), + ('Options apply to all future boots\n' + 'Boot Device Selector : Force PXE\n', + True)] + with task_manager.acquire(self.context, self.node.uuid) as task: + for out, expected_persistent in outputs: + mock_exec.return_value = (out, '') + expected_response = {'boot_device': boot_devices.PXE, + 'persistent': expected_persistent} + self.assertEqual(expected_response, + task.driver.management.get_boot_device(task)) + mock_exec.assert_called_with(mock.ANY, + "chassis bootparam get 5") + + def test_get_force_boot_device_persistent(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_info['ipmi_force_boot_device'] = True + task.node.driver_internal_info['persistent_boot_device'] = 'pxe' + bootdev = self.driver.management.get_boot_device(task) + self.assertEqual('pxe', bootdev['boot_device']) + self.assertTrue(bootdev['persistent']) + + def test_management_interface_validate_good(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.management.validate(task) + + def test_management_interface_validate_fail(self): + # Missing IPMI driver_info information + node = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + driver='fake_ipmitool') + with task_manager.acquire(self.context, node.uuid) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.management.validate, task) + + def test__parse_ipmi_sensor_data_ok(self): + fake_sensors_data = """ + Sensor ID : Temp (0x1) + Entity ID : 3.1 (Processor) + Sensor Type (Analog) : Temperature + Sensor Reading : -58 (+/- 1) degrees C + Status : ok + Nominal Reading : 50.000 + Normal Minimum : 11.000 + Normal Maximum : 69.000 + Upper critical : 90.000 + Upper non-critical : 85.000 + Positive Hysteresis : 1.000 + Negative Hysteresis : 1.000 + + Sensor ID : Temp (0x2) + Entity ID : 3.2 (Processor) + Sensor Type (Analog) : Temperature + Sensor Reading : 50 (+/- 1) degrees C + Status : ok + Nominal Reading : 50.000 + Normal Minimum : 11.000 + Normal Maximum : 69.000 + Upper critical : 90.000 + Upper non-critical : 85.000 + Positive Hysteresis : 1.000 + Negative Hysteresis : 1.000 + + Sensor ID : FAN MOD 1A RPM (0x30) + Entity ID : 7.1 (System Board) + Sensor Type (Analog) : Fan + Sensor Reading : 8400 (+/- 75) RPM + Status : ok + Nominal Reading : 5325.000 + Normal Minimum : 10425.000 + Normal Maximum : 14775.000 + Lower critical : 4275.000 + Positive Hysteresis : 375.000 + Negative Hysteresis : 375.000 + + Sensor ID : FAN MOD 1B RPM (0x31) + Entity ID : 7.1 (System Board) + Sensor Type (Analog) : Fan + Sensor Reading : 8550 (+/- 75) RPM + Status : ok + Nominal Reading : 7800.000 + Normal Minimum : 10425.000 + Normal Maximum : 14775.000 + Lower critical : 4275.000 + Positive Hysteresis : 375.000 + Negative Hysteresis : 375.000 + """ + expected_return = { + 'Fan': { + 'FAN MOD 1A RPM (0x30)': { + 'Status': 'ok', + 'Sensor Reading': '8400 (+/- 75) RPM', + 'Entity ID': '7.1 (System Board)', + 'Normal Minimum': '10425.000', + 'Positive Hysteresis': '375.000', + 'Normal Maximum': '14775.000', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '4275.000', + 'Negative Hysteresis': '375.000', + 'Sensor ID': 'FAN MOD 1A RPM (0x30)', + 'Nominal Reading': '5325.000' + }, + 'FAN MOD 1B RPM (0x31)': { + 'Status': 'ok', + 'Sensor Reading': '8550 (+/- 75) RPM', + 'Entity ID': '7.1 (System Board)', + 'Normal Minimum': '10425.000', + 'Positive Hysteresis': '375.000', + 'Normal Maximum': '14775.000', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '4275.000', + 'Negative Hysteresis': '375.000', + 'Sensor ID': 'FAN MOD 1B RPM (0x31)', + 'Nominal Reading': '7800.000' + } + }, + 'Temperature': { + 'Temp (0x1)': { + 'Status': 'ok', + 'Sensor Reading': '-58 (+/- 1) degrees C', + 'Entity ID': '3.1 (Processor)', + 'Normal Minimum': '11.000', + 'Positive Hysteresis': '1.000', + 'Upper non-critical': '85.000', + 'Normal Maximum': '69.000', + 'Sensor Type (Analog)': 'Temperature', + 'Negative Hysteresis': '1.000', + 'Upper critical': '90.000', + 'Sensor ID': 'Temp (0x1)', + 'Nominal Reading': '50.000' + }, + 'Temp (0x2)': { + 'Status': 'ok', + 'Sensor Reading': '50 (+/- 1) degrees C', + 'Entity ID': '3.2 (Processor)', + 'Normal Minimum': '11.000', + 'Positive Hysteresis': '1.000', + 'Upper non-critical': '85.000', + 'Normal Maximum': '69.000', + 'Sensor Type (Analog)': 'Temperature', + 'Negative Hysteresis': '1.000', + 'Upper critical': '90.000', + 'Sensor ID': 'Temp (0x2)', + 'Nominal Reading': '50.000' + } + } + } + ret = ipmi._parse_ipmi_sensors_data(self.node, fake_sensors_data) + + self.assertEqual(expected_return, ret) + + def test__parse_ipmi_sensor_data_missing_sensor_reading(self): + fake_sensors_data = """ + Sensor ID : Temp (0x1) + Entity ID : 3.1 (Processor) + Sensor Type (Analog) : Temperature + Status : ok + Nominal Reading : 50.000 + Normal Minimum : 11.000 + Normal Maximum : 69.000 + Upper critical : 90.000 + Upper non-critical : 85.000 + Positive Hysteresis : 1.000 + Negative Hysteresis : 1.000 + + Sensor ID : Temp (0x2) + Entity ID : 3.2 (Processor) + Sensor Type (Analog) : Temperature + Sensor Reading : 50 (+/- 1) degrees C + Status : ok + Nominal Reading : 50.000 + Normal Minimum : 11.000 + Normal Maximum : 69.000 + Upper critical : 90.000 + Upper non-critical : 85.000 + Positive Hysteresis : 1.000 + Negative Hysteresis : 1.000 + + Sensor ID : FAN MOD 1A RPM (0x30) + Entity ID : 7.1 (System Board) + Sensor Type (Analog) : Fan + Sensor Reading : 8400 (+/- 75) RPM + Status : ok + Nominal Reading : 5325.000 + Normal Minimum : 10425.000 + Normal Maximum : 14775.000 + Lower critical : 4275.000 + Positive Hysteresis : 375.000 + Negative Hysteresis : 375.000 + """ + expected_return = { + 'Fan': { + 'FAN MOD 1A RPM (0x30)': { + 'Status': 'ok', + 'Sensor Reading': '8400 (+/- 75) RPM', + 'Entity ID': '7.1 (System Board)', + 'Normal Minimum': '10425.000', + 'Positive Hysteresis': '375.000', + 'Normal Maximum': '14775.000', + 'Sensor Type (Analog)': 'Fan', + 'Lower critical': '4275.000', + 'Negative Hysteresis': '375.000', + 'Sensor ID': 'FAN MOD 1A RPM (0x30)', + 'Nominal Reading': '5325.000' + } + }, + 'Temperature': { + 'Temp (0x2)': { + 'Status': 'ok', + 'Sensor Reading': '50 (+/- 1) degrees C', + 'Entity ID': '3.2 (Processor)', + 'Normal Minimum': '11.000', + 'Positive Hysteresis': '1.000', + 'Upper non-critical': '85.000', + 'Normal Maximum': '69.000', + 'Sensor Type (Analog)': 'Temperature', + 'Negative Hysteresis': '1.000', + 'Upper critical': '90.000', + 'Sensor ID': 'Temp (0x2)', + 'Nominal Reading': '50.000' + } + } + } + ret = ipmi._parse_ipmi_sensors_data(self.node, fake_sensors_data) + + self.assertEqual(expected_return, ret) + + def test__parse_ipmi_sensor_data_failed(self): + fake_sensors_data = "abcdef" + self.assertRaises(exception.FailedToParseSensorData, + ipmi._parse_ipmi_sensors_data, + self.node, + fake_sensors_data) + + fake_sensors_data = "abc:def:ghi" + self.assertRaises(exception.FailedToParseSensorData, + ipmi._parse_ipmi_sensors_data, + self.node, + fake_sensors_data) diff --git a/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py b/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py new file mode 100644 index 000000000..261f11766 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py @@ -0,0 +1,1402 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# 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. + +"""Test class for iSCSI deploy mechanism.""" + +import os +import tempfile + +import mock +from oslo_config import cfg +from oslo_utils import fileutils +from oslo_utils import uuidutils + +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import keystone +from ironic.common import pxe_utils +from ironic.common import states +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import agent_base_vendor +from ironic.drivers.modules import agent_client +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules import fake +from ironic.drivers.modules import iscsi_deploy +from ironic.drivers.modules import pxe +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +CONF = cfg.CONF + +INST_INFO_DICT = db_utils.get_test_pxe_instance_info() +DRV_INFO_DICT = db_utils.get_test_pxe_driver_info() +DRV_INTERNAL_INFO_DICT = db_utils.get_test_pxe_driver_internal_info() + + +class IscsiDeployValidateParametersTestCase(db_base.DbTestCase): + + def test_parse_instance_info_good(self): + # make sure we get back the expected things + node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT + ) + info = iscsi_deploy.parse_instance_info(node) + self.assertIsNotNone(info.get('image_source')) + self.assertIsNotNone(info.get('root_gb')) + self.assertEqual(0, info.get('ephemeral_gb')) + self.assertIsNone(info.get('configdrive')) + + def test_parse_instance_info_missing_instance_source(self): + # make sure error is raised when info is missing + info = dict(INST_INFO_DICT) + del info['image_source'] + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.assertRaises(exception.MissingParameterValue, + iscsi_deploy.parse_instance_info, + node) + + def test_parse_instance_info_missing_root_gb(self): + # make sure error is raised when info is missing + info = dict(INST_INFO_DICT) + del info['root_gb'] + + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.assertRaises(exception.MissingParameterValue, + iscsi_deploy.parse_instance_info, + node) + + def test_parse_instance_info_invalid_root_gb(self): + info = dict(INST_INFO_DICT) + info['root_gb'] = 'foobar' + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.parse_instance_info, + node) + + def test_parse_instance_info_valid_ephemeral_gb(self): + ephemeral_gb = 10 + ephemeral_fmt = 'test-fmt' + info = dict(INST_INFO_DICT) + info['ephemeral_gb'] = ephemeral_gb + info['ephemeral_format'] = ephemeral_fmt + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + data = iscsi_deploy.parse_instance_info(node) + self.assertEqual(ephemeral_gb, data.get('ephemeral_gb')) + self.assertEqual(ephemeral_fmt, data.get('ephemeral_format')) + + def test_parse_instance_info_invalid_ephemeral_gb(self): + info = dict(INST_INFO_DICT) + info['ephemeral_gb'] = 'foobar' + info['ephemeral_format'] = 'exttest' + + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.parse_instance_info, + node) + + def test_parse_instance_info_valid_ephemeral_missing_format(self): + ephemeral_gb = 10 + ephemeral_fmt = 'test-fmt' + info = dict(INST_INFO_DICT) + info['ephemeral_gb'] = ephemeral_gb + info['ephemeral_format'] = None + self.config(default_ephemeral_format=ephemeral_fmt, group='pxe') + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + instance_info = iscsi_deploy.parse_instance_info(node) + self.assertEqual(ephemeral_fmt, instance_info['ephemeral_format']) + + def test_parse_instance_info_valid_preserve_ephemeral_true(self): + info = dict(INST_INFO_DICT) + for opt in ['true', 'TRUE', 'True', 't', + 'on', 'yes', 'y', '1']: + info['preserve_ephemeral'] = opt + + node = obj_utils.create_test_node( + self.context, uuid=uuidutils.generate_uuid(), + instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + data = iscsi_deploy.parse_instance_info(node) + self.assertTrue(data.get('preserve_ephemeral')) + + def test_parse_instance_info_valid_preserve_ephemeral_false(self): + info = dict(INST_INFO_DICT) + for opt in ['false', 'FALSE', 'False', 'f', + 'off', 'no', 'n', '0']: + info['preserve_ephemeral'] = opt + node = obj_utils.create_test_node( + self.context, uuid=uuidutils.generate_uuid(), + instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + data = iscsi_deploy.parse_instance_info(node) + self.assertFalse(data.get('preserve_ephemeral')) + + def test_parse_instance_info_invalid_preserve_ephemeral(self): + info = dict(INST_INFO_DICT) + info['preserve_ephemeral'] = 'foobar' + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.parse_instance_info, + node) + + def test_parse_instance_info_invalid_ephemeral_disk(self): + info = dict(INST_INFO_DICT) + info['ephemeral_gb'] = 10 + info['swap_mb'] = 0 + info['root_gb'] = 20 + info['preserve_ephemeral'] = True + drv_internal_dict = {'instance': {'ephemeral_gb': 9, + 'swap_mb': 0, + 'root_gb': 20}} + drv_internal_dict.update(DRV_INTERNAL_INFO_DICT) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=drv_internal_dict, + ) + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.parse_instance_info, + node) + + def test__check_disk_layout_unchanged_fails(self): + info = dict(INST_INFO_DICT) + info['ephemeral_gb'] = 10 + info['swap_mb'] = 0 + info['root_gb'] = 20 + info['preserve_ephemeral'] = True + drv_internal_dict = {'instance': {'ephemeral_gb': 20, + 'swap_mb': 0, + 'root_gb': 20}} + drv_internal_dict.update(DRV_INTERNAL_INFO_DICT) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=drv_internal_dict, + ) + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy._check_disk_layout_unchanged, + node, info) + + def test__check_disk_layout_unchanged(self): + info = dict(INST_INFO_DICT) + info['ephemeral_gb'] = 10 + info['swap_mb'] = 0 + info['root_gb'] = 20 + info['preserve_ephemeral'] = True + drv_internal_dict = {'instance': {'ephemeral_gb': 10, + 'swap_mb': 0, + 'root_gb': 20}} + drv_internal_dict.update(DRV_INTERNAL_INFO_DICT) + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=drv_internal_dict, + ) + self.assertIsNone(iscsi_deploy._check_disk_layout_unchanged(node, + info)) + + def test__save_disk_layout(self): + info = dict(INST_INFO_DICT) + info['ephemeral_gb'] = 10 + info['swap_mb'] = 0 + info['root_gb'] = 10 + info['preserve_ephemeral'] = False + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + iscsi_deploy._save_disk_layout(node, info) + node.refresh() + for param in ('ephemeral_gb', 'swap_mb', 'root_gb'): + self.assertEqual( + info[param], node.driver_internal_info['instance'][param] + ) + + def test_parse_instance_info_configdrive(self): + info = dict(INST_INFO_DICT) + info['configdrive'] = 'http://1.2.3.4/cd' + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + instance_info = iscsi_deploy.parse_instance_info(node) + self.assertEqual('http://1.2.3.4/cd', instance_info['configdrive']) + + def test_parse_instance_info_nonglance_image(self): + info = INST_INFO_DICT.copy() + info['image_source'] = 'file:///image.qcow2' + info['kernel'] = 'file:///image.vmlinuz' + info['ramdisk'] = 'file:///image.initrd' + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + iscsi_deploy.parse_instance_info(node) + + def test_parse_instance_info_nonglance_image_no_kernel(self): + info = INST_INFO_DICT.copy() + info['image_source'] = 'file:///image.qcow2' + info['ramdisk'] = 'file:///image.initrd' + node = obj_utils.create_test_node( + self.context, instance_info=info, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.assertRaises(exception.MissingParameterValue, + iscsi_deploy.parse_instance_info, node) + + def test_parse_instance_info_whole_disk_image(self): + driver_internal_info = dict(DRV_INTERNAL_INFO_DICT) + driver_internal_info['is_whole_disk_image'] = True + node = obj_utils.create_test_node( + self.context, instance_info=INST_INFO_DICT, + driver_internal_info=driver_internal_info, + ) + instance_info = iscsi_deploy.parse_instance_info(node) + self.assertIsNotNone(instance_info.get('image_source')) + self.assertIsNotNone(instance_info.get('root_gb')) + self.assertEqual(0, instance_info.get('swap_mb')) + self.assertEqual(0, instance_info.get('ephemeral_gb')) + self.assertIsNone(instance_info.get('configdrive')) + + def test_parse_instance_info_whole_disk_image_missing_root(self): + info = dict(INST_INFO_DICT) + del info['root_gb'] + node = obj_utils.create_test_node(self.context, instance_info=info) + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.parse_instance_info, node) + + +class IscsiDeployPrivateMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IscsiDeployPrivateMethodsTestCase, self).setUp() + n = { + 'driver': 'fake_pxe', + 'instance_info': INST_INFO_DICT, + 'driver_info': DRV_INFO_DICT, + 'driver_internal_info': DRV_INTERNAL_INFO_DICT, + } + mgr_utils.mock_the_extension_manager(driver="fake_pxe") + self.node = obj_utils.create_test_node(self.context, **n) + + def test__get_image_dir_path(self): + self.assertEqual(os.path.join(CONF.pxe.images_path, + self.node.uuid), + iscsi_deploy._get_image_dir_path(self.node.uuid)) + + def test__get_image_file_path(self): + self.assertEqual(os.path.join(CONF.pxe.images_path, + self.node.uuid, + 'disk'), + iscsi_deploy._get_image_file_path(self.node.uuid)) + + +class IscsiDeployMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(IscsiDeployMethodsTestCase, self).setUp() + instance_info = dict(INST_INFO_DICT) + instance_info['deploy_key'] = 'fake-56789' + n = { + 'driver': 'fake_pxe', + 'instance_info': instance_info, + 'driver_info': DRV_INFO_DICT, + 'driver_internal_info': DRV_INTERNAL_INFO_DICT, + } + mgr_utils.mock_the_extension_manager(driver="fake_pxe") + self.node = obj_utils.create_test_node(self.context, **n) + + @mock.patch.object(deploy_utils, 'get_image_mb', autospec=True) + def test_check_image_size(self, get_image_mb_mock): + get_image_mb_mock.return_value = 1000 + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['root_gb'] = 1 + iscsi_deploy.check_image_size(task) + get_image_mb_mock.assert_called_once_with( + iscsi_deploy._get_image_file_path(task.node.uuid)) + + @mock.patch.object(deploy_utils, 'get_image_mb', autospec=True) + def test_check_image_size_fails(self, get_image_mb_mock): + get_image_mb_mock.return_value = 1025 + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['root_gb'] = 1 + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.check_image_size, + task) + get_image_mb_mock.assert_called_once_with( + iscsi_deploy._get_image_file_path(task.node.uuid)) + + @mock.patch.object(deploy_utils, 'fetch_images', autospec=True) + def test_cache_instance_images_master_path(self, mock_fetch_image): + temp_dir = tempfile.mkdtemp() + self.config(images_path=temp_dir, group='pxe') + self.config(instance_master_path=os.path.join(temp_dir, + 'instance_master_path'), + group='pxe') + fileutils.ensure_tree(CONF.pxe.instance_master_path) + + (uuid, image_path) = iscsi_deploy.cache_instance_image(None, self.node) + mock_fetch_image.assert_called_once_with(None, + mock.ANY, + [(uuid, image_path)], True) + self.assertEqual('glance://image_uuid', uuid) + self.assertEqual(os.path.join(temp_dir, + self.node.uuid, + 'disk'), + image_path) + + @mock.patch.object(utils, 'unlink_without_raise', autospec=True) + @mock.patch.object(utils, 'rmtree_without_raise', autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + def test_destroy_images(self, mock_cache, mock_rmtree, mock_unlink): + self.config(images_path='/path', group='pxe') + + iscsi_deploy.destroy_images('uuid') + + mock_cache.return_value.clean_up.assert_called_once_with() + mock_unlink.assert_called_once_with('/path/uuid/disk') + mock_rmtree.assert_called_once_with('/path/uuid') + + def _test_build_deploy_ramdisk_options(self, mock_alnum, api_url, + expected_root_device=None, + expected_boot_option='netboot', + expected_boot_mode='bios'): + fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV' + fake_disk = 'fake-disk' + + self.config(disk_devices=fake_disk, group='pxe') + + mock_alnum.return_value = fake_key + + expected_iqn = 'iqn.2008-10.org.openstack:%s' % self.node.uuid + expected_opts = { + 'iscsi_target_iqn': expected_iqn, + 'deployment_id': self.node.uuid, + 'deployment_key': fake_key, + 'disk': fake_disk, + 'ironic_api_url': api_url, + 'boot_option': expected_boot_option, + 'boot_mode': expected_boot_mode, + 'coreos.configdrive': 0, + } + + if expected_root_device: + expected_opts['root_device'] = expected_root_device + + opts = iscsi_deploy.build_deploy_ramdisk_options(self.node) + + self.assertEqual(expected_opts, opts) + mock_alnum.assert_called_once_with(32) + # assert deploy_key was injected in the node + self.assertIn('deploy_key', self.node.instance_info) + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + @mock.patch.object(utils, 'random_alnum', autospec=True) + def test_build_deploy_ramdisk_options(self, mock_alnum, mock_get_url): + fake_api_url = 'http://127.0.0.1:6385' + self.config(api_url=fake_api_url, group='conductor') + self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url) + + # As we are getting the Ironic api url from the config file + # assert keystone wasn't called + self.assertFalse(mock_get_url.called) + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + @mock.patch.object(utils, 'random_alnum', autospec=True) + def test_build_deploy_ramdisk_options_keystone(self, mock_alnum, + mock_get_url): + fake_api_url = 'http://127.0.0.1:6385' + mock_get_url.return_value = fake_api_url + self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url) + + # As the Ironic api url is not specified in the config file + # assert we are getting it from keystone + mock_get_url.assert_called_once_with() + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + @mock.patch.object(utils, 'random_alnum', autospec=True) + def test_build_deploy_ramdisk_options_root_device(self, mock_alnum, + mock_get_url): + self.node.properties['root_device'] = {'wwn': 123456} + expected = 'wwn=123456' + fake_api_url = 'http://127.0.0.1:6385' + self.config(api_url=fake_api_url, group='conductor') + self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url, + expected_root_device=expected) + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + @mock.patch.object(utils, 'random_alnum', autospec=True) + def test_build_deploy_ramdisk_options_boot_option(self, mock_alnum, + mock_get_url): + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + expected = 'local' + fake_api_url = 'http://127.0.0.1:6385' + self.config(api_url=fake_api_url, group='conductor') + self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url, + expected_boot_option=expected) + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + @mock.patch.object(utils, 'random_alnum', autospec=True) + def test_build_deploy_ramdisk_options_whole_disk_image(self, mock_alnum, + mock_get_url): + """Tests a hack to boot_option for whole disk images. + + This hack is in place to fix bug #1441556. + """ + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + dii = self.node.driver_internal_info + dii['is_whole_disk_image'] = True + self.node.driver_internal_info = dii + self.node.save() + expected = 'netboot' + fake_api_url = 'http://127.0.0.1:6385' + self.config(api_url=fake_api_url, group='conductor') + self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url, + expected_boot_option=expected) + + @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + def test_continue_deploy_fail(self, deploy_mock, power_mock, + mock_image_cache, mock_disk_layout): + kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} + deploy_mock.side_effect = iter([ + exception.InstanceDeployFailure("test deploy error")]) + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + params = iscsi_deploy.get_deploy_info(task.node, **kwargs) + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.continue_deploy, + task, **kwargs) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertIsNotNone(task.node.last_error) + deploy_mock.assert_called_once_with(**params) + power_mock.assert_called_once_with(task, states.POWER_OFF) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + self.assertFalse(mock_disk_layout.called) + + @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + def test_continue_deploy_ramdisk_fails(self, deploy_mock, power_mock, + mock_image_cache, mock_disk_layout): + kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789', + 'error': 'test ramdisk error'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.continue_deploy, + task, **kwargs) + self.assertIsNotNone(task.node.last_error) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + power_mock.assert_called_once_with(task, states.POWER_OFF) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + self.assertFalse(deploy_mock.called) + self.assertFalse(mock_disk_layout.called) + + @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + def test_continue_deploy_fail_no_root_uuid_or_disk_id( + self, deploy_mock, power_mock, mock_image_cache, mock_disk_layout): + kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} + deploy_mock.return_value = {} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + params = iscsi_deploy.get_deploy_info(task.node, **kwargs) + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.continue_deploy, + task, **kwargs) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertIsNotNone(task.node.last_error) + deploy_mock.assert_called_once_with(**params) + power_mock.assert_called_once_with(task, states.POWER_OFF) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + self.assertFalse(mock_disk_layout.called) + + @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + def test_continue_deploy_fail_empty_root_uuid( + self, deploy_mock, power_mock, mock_image_cache, mock_disk_layout): + kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} + deploy_mock.return_value = {'root uuid': ''} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + params = iscsi_deploy.get_deploy_info(task.node, **kwargs) + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.continue_deploy, + task, **kwargs) + self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertIsNotNone(task.node.last_error) + deploy_mock.assert_called_once_with(**params) + power_mock.assert_called_once_with(task, states.POWER_OFF) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + self.assertFalse(mock_disk_layout.called) + + @mock.patch.object(iscsi_deploy, '_save_disk_layout', autospec=True) + @mock.patch.object(iscsi_deploy, 'LOG', autospec=True) + @mock.patch.object(iscsi_deploy, 'get_deploy_info', autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + def test_continue_deploy(self, deploy_mock, power_mock, mock_image_cache, + mock_deploy_info, mock_log, mock_disk_layout): + kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + mock_deploy_info.return_value = { + 'address': '123456', + 'boot_option': 'netboot', + 'configdrive': "I've got the power", + 'ephemeral_format': None, + 'ephemeral_mb': 0, + 'image_path': (u'/var/lib/ironic/images/1be26c0b-03f2-4d2e-ae87-' + u'c02d7f33c123/disk'), + 'iqn': 'aaa-bbb', + 'lun': '1', + 'node_uuid': u'1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + 'port': '3260', + 'preserve_ephemeral': True, + 'root_mb': 102400, + 'swap_mb': 0, + } + log_params = mock_deploy_info.return_value.copy() + # Make sure we don't log the full content of the configdrive + log_params['configdrive'] = '***' + expected_dict = { + 'node': self.node.uuid, + 'params': log_params, + } + uuid_dict_returned = {'root uuid': '12345678-87654321'} + deploy_mock.return_value = uuid_dict_returned + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + mock_log.isEnabledFor.return_value = True + retval = iscsi_deploy.continue_deploy(task, **kwargs) + mock_log.debug.assert_called_once_with( + mock.ANY, expected_dict) + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertIsNone(task.node.last_error) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + self.assertEqual(uuid_dict_returned, retval) + mock_disk_layout.assert_called_once_with(task.node, mock.ANY) + + @mock.patch.object(iscsi_deploy, 'LOG', autospec=True) + @mock.patch.object(iscsi_deploy, 'get_deploy_info', autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_disk_image', autospec=True) + def test_continue_deploy_whole_disk_image( + self, deploy_mock, power_mock, mock_image_cache, mock_deploy_info, + mock_log): + kwargs = {'address': '123456', 'iqn': 'aaa-bbb', 'key': 'fake-56789'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + mock_deploy_info.return_value = { + 'address': '123456', + 'image_path': (u'/var/lib/ironic/images/1be26c0b-03f2-4d2e-ae87-' + u'c02d7f33c123/disk'), + 'iqn': 'aaa-bbb', + 'lun': '1', + 'node_uuid': u'1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + 'port': '3260', + } + log_params = mock_deploy_info.return_value.copy() + expected_dict = { + 'node': self.node.uuid, + 'params': log_params, + } + uuid_dict_returned = {'disk identifier': '87654321'} + deploy_mock.return_value = uuid_dict_returned + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_internal_info['is_whole_disk_image'] = True + mock_log.isEnabledFor.return_value = True + retval = iscsi_deploy.continue_deploy(task, **kwargs) + mock_log.debug.assert_called_once_with( + mock.ANY, expected_dict) + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertIsNone(task.node.last_error) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + self.assertEqual(uuid_dict_returned, retval) + + def test_get_deploy_info_boot_option_default(self): + instance_info = self.node.instance_info + instance_info['deploy_key'] = 'key' + self.node.instance_info = instance_info + kwargs = {'address': '1.1.1.1', 'iqn': 'target-iqn', 'key': 'key'} + ret_val = iscsi_deploy.get_deploy_info(self.node, **kwargs) + self.assertEqual('1.1.1.1', ret_val['address']) + self.assertEqual('target-iqn', ret_val['iqn']) + self.assertEqual('netboot', ret_val['boot_option']) + + def test_get_deploy_info_netboot_specified(self): + instance_info = self.node.instance_info + instance_info['deploy_key'] = 'key' + instance_info['capabilities'] = {'boot_option': 'netboot'} + self.node.instance_info = instance_info + kwargs = {'address': '1.1.1.1', 'iqn': 'target-iqn', 'key': 'key'} + ret_val = iscsi_deploy.get_deploy_info(self.node, **kwargs) + self.assertEqual('1.1.1.1', ret_val['address']) + self.assertEqual('target-iqn', ret_val['iqn']) + self.assertEqual('netboot', ret_val['boot_option']) + + def test_get_deploy_info_localboot(self): + instance_info = self.node.instance_info + instance_info['deploy_key'] = 'key' + instance_info['capabilities'] = {'boot_option': 'local'} + self.node.instance_info = instance_info + kwargs = {'address': '1.1.1.1', 'iqn': 'target-iqn', 'key': 'key'} + ret_val = iscsi_deploy.get_deploy_info(self.node, **kwargs) + self.assertEqual('1.1.1.1', ret_val['address']) + self.assertEqual('target-iqn', ret_val['iqn']) + self.assertEqual('local', ret_val['boot_option']) + + @mock.patch.object(iscsi_deploy, 'continue_deploy', autospec=True) + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options', + autospec=True) + def test_do_agent_iscsi_deploy_okay(self, build_options_mock, + continue_deploy_mock): + build_options_mock.return_value = {'deployment_key': 'abcdef', + 'iscsi_target_iqn': 'iqn-qweqwe'} + agent_client_mock = mock.MagicMock(spec_set=agent_client.AgentClient) + agent_client_mock.start_iscsi_target.return_value = { + 'command_status': 'SUCCESS', 'command_error': None} + driver_internal_info = {'agent_url': 'http://1.2.3.4:1234'} + self.node.driver_internal_info = driver_internal_info + self.node.save() + uuid_dict_returned = {'root uuid': 'some-root-uuid'} + continue_deploy_mock.return_value = uuid_dict_returned + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ret_val = iscsi_deploy.do_agent_iscsi_deploy( + task, agent_client_mock) + build_options_mock.assert_called_once_with(task.node) + agent_client_mock.start_iscsi_target.assert_called_once_with( + task.node, 'iqn-qweqwe') + continue_deploy_mock.assert_called_once_with( + task, error=None, iqn='iqn-qweqwe', key='abcdef', + address='1.2.3.4') + self.assertEqual( + 'some-root-uuid', + task.node.driver_internal_info['root_uuid_or_disk_id']) + self.assertEqual(ret_val, uuid_dict_returned) + + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options', + autospec=True) + def test_do_agent_iscsi_deploy_start_iscsi_failure(self, + build_options_mock): + build_options_mock.return_value = {'deployment_key': 'abcdef', + 'iscsi_target_iqn': 'iqn-qweqwe'} + agent_client_mock = mock.MagicMock(spec_set=agent_client.AgentClient) + agent_client_mock.start_iscsi_target.return_value = { + 'command_status': 'FAILED', 'command_error': 'booom'} + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.do_agent_iscsi_deploy, + task, agent_client_mock) + build_options_mock.assert_called_once_with(task.node) + agent_client_mock.start_iscsi_target.assert_called_once_with( + task.node, 'iqn-qweqwe') + self.node.refresh() + self.assertEqual(states.DEPLOYFAIL, self.node.provision_state) + self.assertEqual(states.ACTIVE, self.node.target_provision_state) + self.assertIsNotNone(self.node.last_error) + + def test_validate_pass_bootloader_info_input(self): + params = {'key': 'some-random-key', 'address': '1.2.3.4', + 'error': '', 'status': 'SUCCEEDED'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['deploy_key'] = 'some-random-key' + # Assert that the method doesn't raise + iscsi_deploy.validate_pass_bootloader_info_input(task, params) + + def test_validate_pass_bootloader_info_missing_status(self): + params = {'key': 'some-random-key', 'address': '1.2.3.4'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.MissingParameterValue, + iscsi_deploy.validate_pass_bootloader_info_input, + task, params) + + def test_validate_pass_bootloader_info_missing_key(self): + params = {'status': 'SUCCEEDED', 'address': '1.2.3.4'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.MissingParameterValue, + iscsi_deploy.validate_pass_bootloader_info_input, + task, params) + + def test_validate_pass_bootloader_info_missing_address(self): + params = {'status': 'SUCCEEDED', 'key': 'some-random-key'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.MissingParameterValue, + iscsi_deploy.validate_pass_bootloader_info_input, + task, params) + + def test_validate_pass_bootloader_info_input_invalid_key(self): + params = {'key': 'some-other-key', 'address': '1.2.3.4', + 'status': 'SUCCEEDED'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['deploy_key'] = 'some-random-key' + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.validate_pass_bootloader_info_input, + task, params) + + def test_validate_bootloader_install_status(self): + kwargs = {'key': 'abcdef', 'status': 'SUCCEEDED', 'error': ''} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.instance_info['deploy_key'] = 'abcdef' + # Nothing much to assert except that it shouldn't raise. + iscsi_deploy.validate_bootloader_install_status(task, kwargs) + + @mock.patch.object(deploy_utils, 'set_failed_state', autospec=True) + def test_validate_bootloader_install_status_install_failed( + self, set_fail_state_mock): + kwargs = {'key': 'abcdef', 'status': 'FAILED', 'error': 'some-error'} + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYING + task.node.target_provision_state = states.ACTIVE + task.node.instance_info['deploy_key'] = 'abcdef' + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.validate_bootloader_install_status, + task, kwargs) + set_fail_state_mock.assert_called_once_with(task, mock.ANY) + + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + autospec=True) + def test_finish_deploy(self, notify_mock): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + iscsi_deploy.finish_deploy(task, '1.2.3.4') + notify_mock.assert_called_once_with('1.2.3.4') + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(deploy_utils, 'set_failed_state', autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + autospec=True) + def test_finish_deploy_notify_fails(self, notify_mock, + set_fail_state_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + notify_mock.side_effect = RuntimeError() + self.assertRaises(exception.InstanceDeployFailure, + iscsi_deploy.finish_deploy, task, '1.2.3.4') + set_fail_state_mock.assert_called_once_with(task, mock.ANY) + + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + autospec=True) + def test_finish_deploy_ssh_with_local_boot(self, notify_mock, + node_power_mock): + instance_info = dict(INST_INFO_DICT) + instance_info['capabilities'] = {'boot_option': 'local'} + n = { + 'uuid': uuidutils.generate_uuid(), + 'driver': 'fake_ssh', + 'instance_info': instance_info, + 'provision_state': states.DEPLOYING, + 'target_provision_state': states.ACTIVE, + } + mgr_utils.mock_the_extension_manager(driver="fake_ssh") + node = obj_utils.create_test_node(self.context, **n) + + with task_manager.acquire(self.context, node.uuid, + shared=False) as task: + iscsi_deploy.finish_deploy(task, '1.2.3.4') + notify_mock.assert_called_once_with('1.2.3.4') + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + node_power_mock.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + def test_validate_good_api_url_from_config_file(self, mock_ks): + # not present in the keystone catalog + mock_ks.side_effect = exception.KeystoneFailure + self.config(group='conductor', api_url='http://foo') + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + iscsi_deploy.validate(task) + self.assertFalse(mock_ks.called) + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + def test_validate_good_api_url_from_keystone(self, mock_ks): + # present in the keystone catalog + mock_ks.return_value = 'http://127.0.0.1:1234' + # not present in the config file + self.config(group='conductor', api_url=None) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + iscsi_deploy.validate(task) + mock_ks.assert_called_once_with() + + @mock.patch.object(keystone, 'get_service_url', autospec=True) + def test_validate_fail_no_api_url(self, mock_ks): + # not present in the keystone catalog + mock_ks.side_effect = exception.KeystoneFailure + # not present in the config file + self.config(group='conductor', api_url=None) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.validate, task) + mock_ks.assert_called_once_with() + + def test_validate_invalid_root_device_hints(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties['root_device'] = {'size': 'not-int'} + self.assertRaises(exception.InvalidParameterValue, + iscsi_deploy.validate, task) + + +class ISCSIDeployTestCase(db_base.DbTestCase): + + def setUp(self): + super(ISCSIDeployTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_pxe") + self.driver = driver_factory.get_driver("fake_pxe") + self.driver.vendor = iscsi_deploy.VendorPassthru() + self.node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.node.driver_internal_info['agent_url'] = 'http://1.2.3.4:1234' + self.task = mock.MagicMock(spec=task_manager.TaskManager) + self.task.shared = False + self.task.node = self.node + self.task.driver = self.driver + self.task.context = self.context + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual({}, task.driver.deploy.get_properties()) + + @mock.patch.object(iscsi_deploy, 'validate', autospec=True) + @mock.patch.object(deploy_utils, 'validate_capabilities', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'validate', autospec=True) + def test_validate(self, pxe_validate_mock, + validate_capabilities_mock, validate_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + task.driver.deploy.validate(task) + + pxe_validate_mock.assert_called_once_with(task.driver.boot, task) + validate_capabilities_mock.assert_called_once_with(task.node) + validate_mock.assert_called_once_with(task) + + @mock.patch.object(pxe.PXEBoot, 'prepare_instance', autospec=True) + def test_prepare_node_active(self, prepare_instance_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.ACTIVE + + task.driver.deploy.prepare(task) + + prepare_instance_mock.assert_called_once_with( + task.driver.boot, task) + + @mock.patch.object(deploy_utils, 'build_agent_options', autospec=True) + @mock.patch.object(iscsi_deploy, 'build_deploy_ramdisk_options', + autospec=True) + @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', autospec=True) + def test_prepare_node_deploying(self, mock_prepare_ramdisk, + mock_iscsi_options, mock_agent_options): + mock_iscsi_options.return_value = {'a': 'b'} + mock_agent_options.return_value = {'c': 'd'} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYWAIT + + task.driver.deploy.prepare(task) + + mock_iscsi_options.assert_called_once_with(task.node) + mock_agent_options.assert_called_once_with(task.node) + mock_prepare_ramdisk.assert_called_once_with( + task.driver.boot, task, {'a': 'b', 'c': 'd'}) + + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(iscsi_deploy, 'check_image_size', autospec=True) + @mock.patch.object(iscsi_deploy, 'cache_instance_image', autospec=True) + def test_deploy(self, mock_cache_instance_image, + mock_check_image_size, mock_node_power_action): + with task_manager.acquire(self.context, + self.node.uuid, shared=False) as task: + state = task.driver.deploy.deploy(task) + self.assertEqual(state, states.DEPLOYWAIT) + mock_cache_instance_image.assert_called_once_with( + self.context, task.node) + mock_check_image_size.assert_called_once_with(task) + mock_node_power_action.assert_called_once_with(task, states.REBOOT) + + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + def test_tear_down(self, node_power_action_mock): + with task_manager.acquire(self.context, + self.node.uuid, shared=False) as task: + state = task.driver.deploy.tear_down(task) + self.assertEqual(state, states.DELETED) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + + @mock.patch.object(pxe.PXEBoot, 'clean_up_instance', autospec=True) + @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True) + @mock.patch.object(iscsi_deploy, 'destroy_images', autospec=True) + def test_clean_up(self, destroy_images_mock, clean_up_ramdisk_mock, + clean_up_instance_mock): + with task_manager.acquire(self.context, + self.node.uuid, shared=False) as task: + task.driver.deploy.clean_up(task) + destroy_images_mock.assert_called_once_with(task.node.uuid) + clean_up_ramdisk_mock.assert_called_once_with( + task.driver.boot, task) + clean_up_instance_mock.assert_called_once_with( + task.driver.boot, task) + + +class TestVendorPassthru(db_base.DbTestCase): + + def setUp(self): + super(TestVendorPassthru, self).setUp() + mgr_utils.mock_the_extension_manager() + self.driver = driver_factory.get_driver("fake") + self.driver.vendor = iscsi_deploy.VendorPassthru() + self.node = obj_utils.create_test_node( + self.context, driver='fake', + instance_info=INST_INFO_DICT, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.node.driver_internal_info['agent_url'] = 'http://1.2.3.4:1234' + self.task = mock.MagicMock(spec=task_manager.TaskManager) + self.task.shared = False + self.task.node = self.node + self.task.driver = self.driver + self.task.context = self.context + + def test_validate_good(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info['deploy_key'] = 'fake-56789' + task.driver.vendor.validate(task, method='pass_deploy_info', + address='123456', iqn='aaa-bbb', + key='fake-56789') + + def test_validate_fail(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.vendor.validate, + task, method='pass_deploy_info', + key='fake-56789') + + def test_validate_key_notmatch(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.vendor.validate, + task, method='pass_deploy_info', + address='123456', iqn='aaa-bbb', + key='fake-12345') + + @mock.patch.object(fake.FakeBoot, 'prepare_instance', autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_partition_image', autospec=True) + def _test_pass_deploy_info_deploy(self, is_localboot, mock_deploy, + mock_image_cache, + notify_mock, + fakeboot_prepare_instance_mock): + # set local boot + i_info = self.node.instance_info + if is_localboot: + i_info['capabilities'] = '{"boot_option": "local"}' + + i_info['deploy_key'] = 'fake-56789' + self.node.instance_info = i_info + + self.node.power_state = states.POWER_ON + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + root_uuid = "12345678-1234-1234-1234-1234567890abcxyz" + mock_deploy.return_value = {'root uuid': root_uuid} + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.vendor.pass_deploy_info( + task, address='123456', iqn='aaa-bbb', key='fake-56789') + + self.node.refresh() + self.assertEqual(states.POWER_ON, self.node.power_state) + self.assertIn('root_uuid_or_disk_id', self.node.driver_internal_info) + self.assertIsNone(self.node.last_error) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + notify_mock.assert_called_once_with('123456') + fakeboot_prepare_instance_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(fake.FakeBoot, 'prepare_instance', autospec=True) + @mock.patch.object(deploy_utils, 'notify_ramdisk_to_proceed', + autospec=True) + @mock.patch.object(iscsi_deploy, 'InstanceImageCache', autospec=True) + @mock.patch.object(deploy_utils, 'deploy_disk_image', autospec=True) + def _test_pass_deploy_info_whole_disk_image(self, is_localboot, + mock_deploy, + mock_image_cache, + notify_mock, + fakeboot_prep_inst_mock): + i_info = self.node.instance_info + # set local boot + if is_localboot: + i_info['capabilities'] = '{"boot_option": "local"}' + + i_info['deploy_key'] = 'fake-56789' + self.node.instance_info = i_info + + self.node.power_state = states.POWER_ON + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + + disk_id = '0x12345678' + mock_deploy.return_value = {'disk identifier': disk_id} + + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_internal_info['is_whole_disk_image'] = True + task.driver.vendor.pass_deploy_info(task, address='123456', + iqn='aaa-bbb', + key='fake-56789') + + self.node.refresh() + self.assertEqual(states.POWER_ON, self.node.power_state) + self.assertIsNone(self.node.last_error) + mock_image_cache.assert_called_once_with() + mock_image_cache.return_value.clean_up.assert_called_once_with() + notify_mock.assert_called_once_with('123456') + fakeboot_prep_inst_mock.assert_called_once_with(mock.ANY, task) + + def test_pass_deploy_info_deploy(self): + self._test_pass_deploy_info_deploy(False) + self.assertEqual(states.ACTIVE, self.node.provision_state) + self.assertEqual(states.NOSTATE, self.node.target_provision_state) + + def test_pass_deploy_info_localboot(self): + self._test_pass_deploy_info_deploy(True) + self.assertEqual(states.DEPLOYWAIT, self.node.provision_state) + self.assertEqual(states.ACTIVE, self.node.target_provision_state) + + def test_pass_deploy_info_whole_disk_image(self): + self._test_pass_deploy_info_whole_disk_image(False) + self.assertEqual(states.ACTIVE, self.node.provision_state) + self.assertEqual(states.NOSTATE, self.node.target_provision_state) + + def test_pass_deploy_info_whole_disk_image_localboot(self): + self._test_pass_deploy_info_whole_disk_image(True) + self.assertEqual(states.ACTIVE, self.node.provision_state) + self.assertEqual(states.NOSTATE, self.node.target_provision_state) + + def test_pass_deploy_info_invalid(self): + self.node.power_state = states.POWER_ON + self.node.provision_state = states.AVAILABLE + self.node.target_provision_state = states.NOSTATE + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidState, + task.driver.vendor.pass_deploy_info, + task, address='123456', iqn='aaa-bbb', + key='fake-56789', error='test ramdisk error') + + self.node.refresh() + self.assertEqual(states.AVAILABLE, self.node.provision_state) + self.assertEqual(states.NOSTATE, self.node.target_provision_state) + self.assertEqual(states.POWER_ON, self.node.power_state) + + @mock.patch.object(iscsi_deploy.VendorPassthru, 'pass_deploy_info') + def test_pass_deploy_info_lock_elevated(self, mock_deploy_info): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.vendor.pass_deploy_info( + task, address='123456', iqn='aaa-bbb', key='fake-56789') + + # lock elevated w/o exception + self.assertEqual(1, mock_deploy_info.call_count, + "pass_deploy_info was not called once.") + + def test_vendor_routes(self): + expected = ['heartbeat', 'pass_deploy_info', + 'pass_bootloader_install_info'] + 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(list(vendor_routes))) + + def test_driver_routes(self): + expected = ['lookup'] + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + driver_routes = task.driver.vendor.driver_routes + self.assertIsInstance(driver_routes, dict) + self.assertEqual(sorted(expected), sorted(list(driver_routes))) + + @mock.patch.object(iscsi_deploy, 'validate_bootloader_install_status', + autospec=True) + @mock.patch.object(iscsi_deploy, 'finish_deploy', autospec=True) + def test_pass_bootloader_install_info(self, finish_deploy_mock, + validate_input_mock): + kwargs = {'method': 'pass_deploy_info', 'address': '123456'} + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.vendor.pass_bootloader_install_info(task, **kwargs) + finish_deploy_mock.assert_called_once_with(task, '123456') + validate_input_mock.assert_called_once_with(task, kwargs) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', autospec=True) + def test_continue_deploy_netboot(self, do_agent_iscsi_deploy_mock, + reboot_and_finish_deploy_mock): + + uuid_dict_returned = {'root uuid': 'some-root-uuid'} + do_agent_iscsi_deploy_mock.return_value = uuid_dict_returned + self.driver.vendor.continue_deploy(self.task) + do_agent_iscsi_deploy_mock.assert_called_once_with( + self.task, self.driver.vendor._client) + reboot_and_finish_deploy_mock.assert_called_once_with( + mock.ANY, self.task) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', autospec=True) + def test_continue_deploy_localboot(self, do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + reboot_and_finish_deploy_mock): + + self.node.instance_info = { + 'capabilities': {'boot_option': 'local'}} + self.node.save() + uuid_dict_returned = {'root uuid': 'some-root-uuid'} + do_agent_iscsi_deploy_mock.return_value = uuid_dict_returned + + self.driver.vendor.continue_deploy(self.task) + do_agent_iscsi_deploy_mock.assert_called_once_with( + self.task, self.driver.vendor._client) + configure_local_boot_mock.assert_called_once_with( + self.task.driver.vendor, self.task, root_uuid='some-root-uuid', + efi_system_part_uuid=None) + reboot_and_finish_deploy_mock.assert_called_once_with( + self.task.driver.vendor, self.task) + + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'reboot_and_finish_deploy', autospec=True) + @mock.patch.object(agent_base_vendor.BaseAgentVendor, + 'configure_local_boot', autospec=True) + @mock.patch.object(iscsi_deploy, 'do_agent_iscsi_deploy', autospec=True) + def test_continue_deploy_localboot_uefi(self, do_agent_iscsi_deploy_mock, + configure_local_boot_mock, + reboot_and_finish_deploy_mock): + + self.node.instance_info = { + 'capabilities': {'boot_option': 'local'}} + self.node.save() + uuid_dict_returned = {'root uuid': 'some-root-uuid', + 'efi system partition uuid': 'efi-part-uuid'} + do_agent_iscsi_deploy_mock.return_value = uuid_dict_returned + + self.driver.vendor.continue_deploy(self.task) + do_agent_iscsi_deploy_mock.assert_called_once_with( + self.task, self.driver.vendor._client) + configure_local_boot_mock.assert_called_once_with( + self.task.driver.vendor, self.task, root_uuid='some-root-uuid', + efi_system_part_uuid='efi-part-uuid') + reboot_and_finish_deploy_mock.assert_called_once_with( + self.task.driver.vendor, self.task) + + +# Cleanup of iscsi_deploy with pxe boot interface +class CleanUpFullFlowTestCase(db_base.DbTestCase): + def setUp(self): + super(CleanUpFullFlowTestCase, self).setUp() + self.config(image_cache_size=0, group='pxe') + + # Configure node + mgr_utils.mock_the_extension_manager(driver="fake_pxe") + instance_info = INST_INFO_DICT + instance_info['deploy_key'] = 'fake-56789' + self.node = obj_utils.create_test_node( + self.context, driver='fake_pxe', + instance_info=instance_info, + driver_info=DRV_INFO_DICT, + driver_internal_info=DRV_INTERNAL_INFO_DICT, + ) + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + + # Configure temporary directories + pxe_temp_dir = tempfile.mkdtemp() + self.config(tftp_root=pxe_temp_dir, group='pxe') + tftp_master_dir = os.path.join(CONF.pxe.tftp_root, + 'tftp_master') + self.config(tftp_master_path=tftp_master_dir, group='pxe') + os.makedirs(tftp_master_dir) + + instance_temp_dir = tempfile.mkdtemp() + self.config(images_path=instance_temp_dir, + group='pxe') + instance_master_dir = os.path.join(CONF.pxe.images_path, + 'instance_master') + self.config(instance_master_path=instance_master_dir, + group='pxe') + os.makedirs(instance_master_dir) + self.pxe_config_dir = os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg') + os.makedirs(self.pxe_config_dir) + + # Populate some file names + self.master_kernel_path = os.path.join(CONF.pxe.tftp_master_path, + 'kernel') + self.master_instance_path = os.path.join(CONF.pxe.instance_master_path, + 'image_uuid') + self.node_tftp_dir = os.path.join(CONF.pxe.tftp_root, + self.node.uuid) + os.makedirs(self.node_tftp_dir) + self.kernel_path = os.path.join(self.node_tftp_dir, + 'kernel') + self.node_image_dir = iscsi_deploy._get_image_dir_path(self.node.uuid) + os.makedirs(self.node_image_dir) + self.image_path = iscsi_deploy._get_image_file_path(self.node.uuid) + self.config_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) + self.mac_path = pxe_utils._get_pxe_mac_path(self.port.address) + + # Create files + self.files = [self.config_path, self.master_kernel_path, + self.master_instance_path] + for fname in self.files: + # NOTE(dtantsur): files with 0 size won't be cleaned up + with open(fname, 'w') as fp: + fp.write('test') + + os.link(self.config_path, self.mac_path) + os.link(self.master_kernel_path, self.kernel_path) + os.link(self.master_instance_path, self.image_path) + + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + @mock.patch.object(pxe, '_get_deploy_image_info', autospec=True) + def test_clean_up_with_master(self, mock_get_deploy_image_info, + mock_get_instance_image_info): + image_info = {'kernel': ('kernel_uuid', + self.kernel_path)} + mock_get_instance_image_info.return_value = image_info + mock_get_deploy_image_info.return_value = {} + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.deploy.clean_up(task) + mock_get_instance_image_info.assert_called_with(task.node, + task.context) + mock_get_deploy_image_info.assert_called_with(task.node) + for path in ([self.kernel_path, self.image_path, self.config_path] + + self.files): + self.assertFalse(os.path.exists(path), + '%s is not expected to exist' % path) diff --git a/ironic/tests/unit/drivers/modules/test_seamicro.py b/ironic/tests/unit/drivers/modules/test_seamicro.py new file mode 100644 index 000000000..63d816112 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_seamicro.py @@ -0,0 +1,676 @@ +# 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. + +"""Test class for Ironic SeaMicro driver.""" + +import uuid + +import mock +from oslo_utils import uuidutils +from seamicroclient import client as seamicro_client +from seamicroclient import exceptions as seamicro_client_exception + +from ironic.common import boot_devices +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import console_utils +from ironic.drivers.modules import seamicro +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +INFO_DICT = db_utils.get_test_seamicro_info() + + +class Fake_Server(object): + def __init__(self, active=False, *args, **kwargs): + self.active = active + self.nic = {'0': {'untaggedVlan': ''}} + + def power_on(self): + self.active = True + + def power_off(self, force=False): + self.active = False + + def reset(self): + self.active = True + + def set_untagged_vlan(self, vlan_id): + return + + def attach_volume(self, volume_id): + return + + def detach_volume(self): + return + + def set_boot_order(self, boot_order): + return + + def refresh(self, wait=0): + return self + + +class Fake_Volume(object): + def __init__(self, id=None, *args, **kwargs): + if id is None: + self.id = "%s/%s/%s" % ("0", "ironic-p6-6", str(uuid.uuid4())) + else: + self.id = id + + +class Fake_Pool(object): + def __init__(self, freeSize=None, *args, **kwargs): + self.freeSize = freeSize + + +class SeaMicroValidateParametersTestCase(db_base.DbTestCase): + + def test__parse_driver_info_good(self): + # make sure we get back the expected things + node = obj_utils.get_test_node( + self.context, + driver='fake_seamicro', + driver_info=INFO_DICT) + info = seamicro._parse_driver_info(node) + self.assertIsNotNone(info.get('api_endpoint')) + self.assertIsNotNone(info.get('username')) + self.assertIsNotNone(info.get('password')) + self.assertIsNotNone(info.get('server_id')) + self.assertIsNotNone(info.get('uuid')) + + def test__parse_driver_info_missing_api_endpoint(self): + # make sure error is raised when info is missing + info = dict(INFO_DICT) + del info['seamicro_api_endpoint'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + seamicro._parse_driver_info, + node) + + def test__parse_driver_info_missing_username(self): + # make sure error is raised when info is missing + info = dict(INFO_DICT) + del info['seamicro_username'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + seamicro._parse_driver_info, + node) + + def test__parse_driver_info_missing_password(self): + # make sure error is raised when info is missing + info = dict(INFO_DICT) + del info['seamicro_password'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + seamicro._parse_driver_info, + node) + + def test__parse_driver_info_missing_server_id(self): + # make sure error is raised when info is missing + info = dict(INFO_DICT) + del info['seamicro_server_id'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + seamicro._parse_driver_info, + node) + + +@mock.patch('eventlet.greenthread.sleep', lambda n: None) +class SeaMicroPrivateMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(SeaMicroPrivateMethodsTestCase, self).setUp() + n = { + 'driver': 'fake_seamicro', + 'driver_info': INFO_DICT + } + self.node = obj_utils.create_test_node(self.context, **n) + self.Server = Fake_Server + self.Volume = Fake_Volume + self.Pool = Fake_Pool + self.config(action_timeout=0, group='seamicro') + self.config(max_retry=2, group='seamicro') + + self.info = seamicro._parse_driver_info(self.node) + + @mock.patch.object(seamicro_client, "Client", autospec=True) + def test__get_client(self, mock_client): + args = {'username': self.info['username'], + 'password': self.info['password'], + 'auth_url': self.info['api_endpoint']} + seamicro._get_client(**self.info) + mock_client.assert_called_once_with(self.info['api_version'], **args) + + @mock.patch.object(seamicro_client, "Client", autospec=True) + def test__get_client_fail(self, mock_client): + args = {'username': self.info['username'], + 'password': self.info['password'], + 'auth_url': self.info['api_endpoint']} + mock_client.side_effect = seamicro_client_exception.UnsupportedVersion + self.assertRaises(exception.InvalidParameterValue, + seamicro._get_client, + **self.info) + mock_client.assert_called_once_with(self.info['api_version'], **args) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__get_power_status_on(self, mock_get_server): + mock_get_server.return_value = self.Server(active=True) + pstate = seamicro._get_power_status(self.node) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__get_power_status_off(self, mock_get_server): + mock_get_server.return_value = self.Server(active=False) + pstate = seamicro._get_power_status(self.node) + self.assertEqual(states.POWER_OFF, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__get_power_status_error(self, mock_get_server): + mock_get_server.return_value = self.Server(active=None) + pstate = seamicro._get_power_status(self.node) + self.assertEqual(states.ERROR, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__power_on_good(self, mock_get_server): + mock_get_server.return_value = self.Server(active=False) + pstate = seamicro._power_on(self.node) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__power_on_fail(self, mock_get_server): + def fake_power_on(): + return + + server = self.Server(active=False) + server.power_on = fake_power_on + mock_get_server.return_value = server + pstate = seamicro._power_on(self.node) + self.assertEqual(states.ERROR, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__power_off_good(self, mock_get_server): + mock_get_server.return_value = self.Server(active=True) + pstate = seamicro._power_off(self.node) + self.assertEqual(states.POWER_OFF, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__power_off_fail(self, mock_get_server): + def fake_power_off(): + return + server = self.Server(active=True) + server.power_off = fake_power_off + mock_get_server.return_value = server + pstate = seamicro._power_off(self.node) + self.assertEqual(states.ERROR, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__reboot_good(self, mock_get_server): + mock_get_server.return_value = self.Server(active=True) + pstate = seamicro._reboot(self.node) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch.object(seamicro, "_get_server", autospec=True) + def test__reboot_fail(self, mock_get_server): + def fake_reboot(): + return + server = self.Server(active=False) + server.reset = fake_reboot + mock_get_server.return_value = server + pstate = seamicro._reboot(self.node) + self.assertEqual(states.ERROR, pstate) + + @mock.patch.object(seamicro, "_get_volume", autospec=True) + def test__validate_fail(self, mock_get_volume): + volume_id = "0/p6-6/vol1" + volume = self.Volume() + volume.id = volume_id + mock_get_volume.return_value = volume + self.assertRaises(exception.InvalidParameterValue, + seamicro._validate_volume, self.info, volume_id) + + @mock.patch.object(seamicro, "_get_volume", autospec=True) + def test__validate_good(self, mock_get_volume): + volume = self.Volume() + mock_get_volume.return_value = volume + valid = seamicro._validate_volume(self.info, volume.id) + self.assertEqual(valid, True) + + @mock.patch.object(seamicro, "_get_pools", autospec=True) + def test__create_volume_fail(self, mock_get_pools): + mock_get_pools.return_value = None + self.assertRaises(exception.IronicException, + seamicro._create_volume, + self.info, 2) + + @mock.patch.object(seamicro, "_get_pools", autospec=True) + @mock.patch.object(seamicro, "_get_client", autospec=True) + def test__create_volume_good(self, mock_get_client, mock_get_pools): + pools = [self.Pool(1), self.Pool(6), self.Pool(5)] + mock_seamicro_volumes = mock.MagicMock(spec_set=['create']) + mock_get_client.return_value = mock.MagicMock( + volumes=mock_seamicro_volumes, spec_set=['volumes']) + mock_get_pools.return_value = pools + seamicro._create_volume(self.info, 2) + + +class SeaMicroPowerDriverTestCase(db_base.DbTestCase): + + def setUp(self): + super(SeaMicroPowerDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_seamicro') + self.driver = driver_factory.get_driver('fake_seamicro') + self.node = obj_utils.create_test_node(self.context, + driver='fake_seamicro', + driver_info=INFO_DICT) + self.get_server_patcher = mock.patch.object(seamicro, '_get_server', + autospec=True) + + self.get_server_mock = None + self.Server = Fake_Server + self.Volume = Fake_Volume + self.info = seamicro._parse_driver_info(self.node) + + def test_get_properties(self): + expected = seamicro.COMMON_PROPERTIES + with task_manager.acquire(self.context, self.node['uuid'], + shared=True) as task: + self.assertEqual(expected, task.driver.power.get_properties()) + + expected = (list(seamicro.COMMON_PROPERTIES) + + list(seamicro.CONSOLE_PROPERTIES)) + console_properties = task.driver.console.get_properties().keys() + self.assertEqual(sorted(expected), sorted(console_properties)) + self.assertEqual(sorted(expected), + sorted(task.driver.get_properties().keys())) + + def test_vendor_routes(self): + expected = ['set_node_vlan_id', 'attach_volume'] + 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)) + + def test_driver_routes(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + driver_routes = task.driver.vendor.driver_routes + self.assertIsInstance(driver_routes, dict) + self.assertEqual({}, driver_routes) + + @mock.patch.object(seamicro, '_parse_driver_info', autospec=True) + def test_power_interface_validate_good(self, parse_drv_info_mock): + with task_manager.acquire(self.context, self.node['uuid'], + shared=True) as task: + task.driver.power.validate(task) + self.assertEqual(1, parse_drv_info_mock.call_count) + + @mock.patch.object(seamicro, '_parse_driver_info', autospec=True) + def test_power_interface_validate_fails(self, parse_drv_info_mock): + side_effect = iter([exception.InvalidParameterValue("Bad input")]) + parse_drv_info_mock.side_effect = side_effect + with task_manager.acquire(self.context, self.node['uuid'], + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, task) + self.assertEqual(1, parse_drv_info_mock.call_count) + + @mock.patch.object(seamicro, '_reboot', autospec=True) + def test_reboot(self, mock_reboot): + mock_reboot.return_value = states.POWER_ON + + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + task.driver.power.reboot(task) + + mock_reboot.assert_called_once_with(task.node) + + def test_set_power_state_bad_state(self): + self.get_server_mock = self.get_server_patcher.start() + self.get_server_mock.return_value = self.Server() + + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + self.assertRaises(exception.IronicException, + task.driver.power.set_power_state, + task, "BAD_PSTATE") + self.get_server_patcher.stop() + + @mock.patch.object(seamicro, '_power_on', autospec=True) + def test_set_power_state_on_good(self, mock_power_on): + mock_power_on.return_value = states.POWER_ON + + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + + mock_power_on.assert_called_once_with(task.node) + + @mock.patch.object(seamicro, '_power_on', autospec=True) + def test_set_power_state_on_fail(self, mock_power_on): + mock_power_on.return_value = states.POWER_OFF + + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.set_power_state, + task, states.POWER_ON) + + mock_power_on.assert_called_once_with(task.node) + + @mock.patch.object(seamicro, '_power_off', autospec=True) + def test_set_power_state_off_good(self, mock_power_off): + mock_power_off.return_value = states.POWER_OFF + + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_OFF) + + mock_power_off.assert_called_once_with(task.node) + + @mock.patch.object(seamicro, '_power_off', autospec=True) + def test_set_power_state_off_fail(self, mock_power_off): + mock_power_off.return_value = states.POWER_ON + + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.set_power_state, + task, states.POWER_OFF) + + mock_power_off.assert_called_once_with(task.node) + + @mock.patch.object(seamicro, '_parse_driver_info', autospec=True) + def test_vendor_passthru_validate_good(self, mock_info): + with task_manager.acquire(self.context, self.node['uuid'], + shared=True) as task: + for method in task.driver.vendor.vendor_routes: + task.driver.vendor.validate(task, **{'method': method}) + self.assertEqual(len(task.driver.vendor.vendor_routes), + mock_info.call_count) + + @mock.patch.object(seamicro, '_parse_driver_info', autospec=True) + def test_vendor_passthru_validate_parse_driver_info_fail(self, mock_info): + mock_info.side_effect = iter([exception.InvalidParameterValue("bad")]) + with task_manager.acquire(self.context, self.node['uuid'], + shared=True) as task: + method = list(task.driver.vendor.vendor_routes)[0] + self.assertRaises(exception.InvalidParameterValue, + task.driver.vendor.validate, + task, **{'method': method}) + mock_info.assert_called_once_with(task.node) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + def test_set_node_vlan_id_good(self, mock_get_server): + vlan_id = "12" + mock_get_server.return_value = self.Server(active="true") + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + kwargs = {'vlan_id': vlan_id} + task.driver.vendor.set_node_vlan_id(task, **kwargs) + mock_get_server.assert_called_once_with(self.info) + + def test_set_node_vlan_id_no_input(self): + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.vendor.set_node_vlan_id, + task, **{}) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + def test_set_node_vlan_id_fail(self, mock_get_server): + def fake_set_untagged_vlan(self, **kwargs): + raise seamicro_client_exception.ClientException(500) + + vlan_id = "12" + server = self.Server(active="true") + server.set_untagged_vlan = fake_set_untagged_vlan + mock_get_server.return_value = server + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + kwargs = {'vlan_id': vlan_id} + self.assertRaises(exception.IronicException, + task.driver.vendor.set_node_vlan_id, + task, **kwargs) + + mock_get_server.assert_called_once_with(self.info) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + @mock.patch.object(seamicro, '_validate_volume', autospec=True) + def test_attach_volume_with_volume_id_good(self, mock_validate_volume, + mock_get_server): + volume_id = '0/ironic-p6-1/vol1' + mock_validate_volume.return_value = True + mock_get_server.return_value = self.Server(active="true") + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + kwargs = {'volume_id': volume_id} + task.driver.vendor.attach_volume(task, **kwargs) + mock_get_server.assert_called_once_with(self.info) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + @mock.patch.object(seamicro, '_get_volume', autospec=True) + def test_attach_volume_with_invalid_volume_id_fail(self, + mock_get_volume, + mock_get_server): + volume_id = '0/p6-1/vol1' + mock_get_volume.return_value = self.Volume(volume_id) + mock_get_server.return_value = self.Server(active="true") + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + kwargs = {'volume_id': volume_id} + self.assertRaises(exception.InvalidParameterValue, + task.driver.vendor.attach_volume, + task, **kwargs) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + @mock.patch.object(seamicro, '_validate_volume', autospec=True) + def test_attach_volume_fail(self, mock_validate_volume, + mock_get_server): + def fake_attach_volume(self, **kwargs): + raise seamicro_client_exception.ClientException(500) + + volume_id = '0/p6-1/vol1' + mock_validate_volume.return_value = True + server = self.Server(active="true") + server.attach_volume = fake_attach_volume + mock_get_server.return_value = server + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + kwargs = {'volume_id': volume_id} + self.assertRaises(exception.IronicException, + task.driver.vendor.attach_volume, + task, **kwargs) + + mock_get_server.assert_called_once_with(self.info) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + @mock.patch.object(seamicro, '_validate_volume', autospec=True) + @mock.patch.object(seamicro, '_create_volume', autospec=True) + def test_attach_volume_with_volume_size_good(self, mock_create_volume, + mock_validate_volume, + mock_get_server): + volume_id = '0/ironic-p6-1/vol1' + volume_size = 2 + mock_create_volume.return_value = volume_id + mock_validate_volume.return_value = True + mock_get_server.return_value = self.Server(active="true") + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + kwargs = {'volume_size': volume_size} + task.driver.vendor.attach_volume(task, **kwargs) + mock_get_server.assert_called_once_with(self.info) + mock_create_volume.assert_called_once_with(self.info, volume_size) + + def test_attach_volume_with_no_input_fail(self): + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.vendor.attach_volume, task, + **{}) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + def test_set_boot_device_good(self, mock_get_server): + boot_device = "disk" + mock_get_server.return_value = self.Server(active="true") + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + task.driver.management.set_boot_device(task, boot_device) + mock_get_server.assert_called_once_with(self.info) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + def test_set_boot_device_invalid_device_fail(self, mock_get_server): + boot_device = "invalid_device" + mock_get_server.return_value = self.Server(active="true") + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.management.set_boot_device, + task, boot_device) + + @mock.patch.object(seamicro, '_get_server', autospec=True) + def test_set_boot_device_fail(self, mock_get_server): + def fake_set_boot_order(self, **kwargs): + raise seamicro_client_exception.ClientException(500) + + boot_device = "pxe" + server = self.Server(active="true") + server.set_boot_order = fake_set_boot_order + mock_get_server.return_value = server + with task_manager.acquire(self.context, self.info['uuid'], + shared=False) as task: + self.assertRaises(exception.IronicException, + task.driver.management.set_boot_device, + task, boot_device) + + mock_get_server.assert_called_once_with(self.info) + + def test_management_interface_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + expected = [boot_devices.PXE, boot_devices.DISK] + self.assertEqual(sorted(expected), sorted(task.driver.management. + get_supported_boot_devices(task))) + + def test_management_interface_get_boot_device(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + expected = {'boot_device': None, 'persistent': None} + self.assertEqual(expected, + task.driver.management.get_boot_device(task)) + + def test_management_interface_validate_good(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.management.validate(task) + + def test_management_interface_validate_fail(self): + # Missing SEAMICRO driver_info information + node = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + driver='fake_seamicro') + with task_manager.acquire(self.context, node.uuid) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.management.validate, task) + + +class SeaMicroDriverTestCase(db_base.DbTestCase): + + def setUp(self): + super(SeaMicroDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_seamicro') + self.driver = driver_factory.get_driver('fake_seamicro') + self.node = obj_utils.create_test_node(self.context, + driver='fake_seamicro', + driver_info=INFO_DICT) + self.get_server_patcher = mock.patch.object(seamicro, '_get_server', + autospec=True) + + self.get_server_mock = None + self.Server = Fake_Server + self.Volume = Fake_Volume + self.info = seamicro._parse_driver_info(self.node) + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console(self, mock_exec): + mock_exec.return_value = None + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.console.start_console(task) + + mock_exec.assert_called_once_with(self.info['uuid'], + self.info['port'], + mock.ANY) + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console_fail(self, mock_exec): + mock_exec.side_effect = iter( + [exception.ConsoleSubprocessFailed(error='error')]) + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.ConsoleSubprocessFailed, + self.driver.console.start_console, + task) + + @mock.patch.object(console_utils, 'stop_shellinabox_console', + autospec=True) + def test_stop_console(self, mock_exec): + mock_exec.return_value = None + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.driver.console.stop_console(task) + + mock_exec.assert_called_once_with(self.info['uuid']) + + @mock.patch.object(console_utils, 'stop_shellinabox_console', + autospec=True) + def test_stop_console_fail(self, mock_stop): + mock_stop.side_effect = iter([exception.ConsoleError()]) + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.ConsoleError, + self.driver.console.stop_console, + task) + + mock_stop.assert_called_once_with(self.node.uuid) + + @mock.patch.object(console_utils, 'start_shellinabox_console', + autospec=True) + def test_start_console_fail_nodir(self, mock_exec): + mock_exec.side_effect = iter([exception.ConsoleError()]) + + with task_manager.acquire(self.context, + self.node.uuid) as task: + self.assertRaises(exception.ConsoleError, + self.driver.console.start_console, + task) + mock_exec.assert_called_once_with(self.node.uuid, mock.ANY, mock.ANY) + + @mock.patch.object(console_utils, 'get_shellinabox_console_url', + autospec=True) + def test_get_console(self, mock_exec): + url = 'http://localhost:4201' + mock_exec.return_value = url + expected = {'type': 'shellinabox', 'url': url} + + with task_manager.acquire(self.context, + self.node.uuid) as task: + console_info = self.driver.console.get_console(task) + + self.assertEqual(expected, console_info) + mock_exec.assert_called_once_with(self.info['port']) diff --git a/ironic/tests/unit/drivers/modules/test_snmp.py b/ironic/tests/unit/drivers/modules/test_snmp.py new file mode 100644 index 000000000..5842739f0 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_snmp.py @@ -0,0 +1,1263 @@ +# Copyright 2013,2014 Cray Inc +# +# Authors: David Hewson <dhewson@cray.com> +# Stig Telfer <stelfer@cray.com> +# Mark Goddard <mgoddard@cray.com> +# +# 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. + +"""Test class for SNMP power driver module.""" + +import mock +from oslo_config import cfg +from pysnmp.entity.rfc3413.oneliner import cmdgen +from pysnmp import error as snmp_error + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import snmp as snmp +from ironic.tests.unit import base +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +CONF = cfg.CONF +INFO_DICT = db_utils.get_test_snmp_info() + + +@mock.patch.object(cmdgen, 'CommandGenerator', autospec=True) +class SNMPClientTestCase(base.TestCase): + def setUp(self): + super(SNMPClientTestCase, self).setUp() + self.address = '1.2.3.4' + self.port = '6700' + self.oid = 'oid' + self.value = 'value' + + def test___init__(self, mock_cmdgen): + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V1) + mock_cmdgen.assert_called_once_with() + self.assertEqual(self.address, client.address) + self.assertEqual(self.port, client.port) + self.assertEqual(snmp.SNMP_V1, client.version) + self.assertIsNone(client.community) + self.assertFalse('security' in client.__dict__) + self.assertEqual(mock_cmdgen.return_value, client.cmd_gen) + + @mock.patch.object(cmdgen, 'CommunityData', autospec=True) + def test__get_auth_v1(self, mock_community, mock_cmdgen): + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V1) + client._get_auth() + mock_cmdgen.assert_called_once_with() + mock_community.assert_called_once_with(client.community, mpModel=0) + + @mock.patch.object(cmdgen, 'UsmUserData', autospec=True) + def test__get_auth_v3(self, mock_user, mock_cmdgen): + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + client._get_auth() + mock_cmdgen.assert_called_once_with() + mock_user.assert_called_once_with(client.security) + + @mock.patch.object(cmdgen, 'UdpTransportTarget', autospec=True) + def test__get_transport(self, mock_transport, mock_cmdgen): + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + client._get_transport() + mock_cmdgen.assert_called_once_with() + mock_transport.assert_called_once_with((client.address, client.port)) + + @mock.patch.object(cmdgen, 'UdpTransportTarget', autospec=True) + def test__get_transport_err(self, mock_transport, mock_cmdgen): + mock_transport.side_effect = snmp_error.PySnmpError + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + self.assertRaises(snmp_error.PySnmpError, client._get_transport) + mock_cmdgen.assert_called_once_with() + mock_transport.assert_called_once_with((client.address, client.port)) + + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_get(self, mock_auth, mock_transport, mock_cmdgen): + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.getCmd.return_value = ("", None, 0, [var_bind]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + val = client.get(self.oid) + self.assertEqual(var_bind[1], val) + mock_cmdgenerator.getCmd.assert_called_once_with(mock.ANY, mock.ANY, + self.oid) + + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_get_err_transport(self, mock_auth, mock_transport, mock_cmdgen): + mock_transport.side_effect = snmp_error.PySnmpError + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.getCmd.return_value = ("engine error", None, 0, + [var_bind]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + self.assertRaises(exception.SNMPFailure, client.get, self.oid) + self.assertFalse(mock_cmdgenerator.getCmd.called) + + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_get_err_engine(self, mock_auth, mock_transport, mock_cmdgen): + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.getCmd.return_value = ("engine error", None, 0, + [var_bind]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + self.assertRaises(exception.SNMPFailure, client.get, self.oid) + mock_cmdgenerator.getCmd.assert_called_once_with(mock.ANY, mock.ANY, + self.oid) + + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_set(self, mock_auth, mock_transport, mock_cmdgen): + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.setCmd.return_value = ("", None, 0, [var_bind]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + client.set(self.oid, self.value) + mock_cmdgenerator.setCmd.assert_called_once_with(mock.ANY, mock.ANY, + var_bind) + + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_set_err_transport(self, mock_auth, mock_transport, mock_cmdgen): + mock_transport.side_effect = snmp_error.PySnmpError + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.setCmd.return_value = ("engine error", None, 0, + [var_bind]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + self.assertRaises(exception.SNMPFailure, + client.set, self.oid, self.value) + self.assertFalse(mock_cmdgenerator.setCmd.called) + + @mock.patch.object(snmp.SNMPClient, '_get_transport', autospec=True) + @mock.patch.object(snmp.SNMPClient, '_get_auth', autospec=True) + def test_set_err_engine(self, mock_auth, mock_transport, mock_cmdgen): + var_bind = (self.oid, self.value) + mock_cmdgenerator = mock_cmdgen.return_value + mock_cmdgenerator.setCmd.return_value = ("engine error", None, 0, + [var_bind]) + client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) + self.assertRaises(exception.SNMPFailure, + client.set, self.oid, self.value) + mock_cmdgenerator.setCmd.assert_called_once_with(mock.ANY, mock.ANY, + var_bind) + + +class SNMPValidateParametersTestCase(db_base.DbTestCase): + + def _get_test_node(self, driver_info): + return obj_utils.get_test_node( + self.context, + driver_info=driver_info) + + def test__parse_driver_info_default(self): + # Make sure we get back the expected things. + node = self._get_test_node(INFO_DICT) + info = snmp._parse_driver_info(node) + self.assertEqual(INFO_DICT['snmp_driver'], info.get('driver')) + self.assertEqual(INFO_DICT['snmp_address'], info.get('address')) + self.assertEqual(INFO_DICT['snmp_port'], str(info.get('port'))) + self.assertEqual(INFO_DICT['snmp_outlet'], info.get('outlet')) + self.assertEqual(INFO_DICT['snmp_version'], info.get('version')) + self.assertEqual(INFO_DICT.get('snmp_community'), + info.get('community')) + self.assertEqual(INFO_DICT.get('snmp_security'), + info.get('security')) + + def test__parse_driver_info_apc(self): + # Make sure the APC driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='apc') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('apc', info.get('driver')) + + def test__parse_driver_info_apc_masterswitch(self): + # Make sure the APC driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='apc_masterswitch') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('apc_masterswitch', info.get('driver')) + + def test__parse_driver_info_apc_masterswitchplus(self): + # Make sure the APC driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='apc_masterswitchplus') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('apc_masterswitchplus', info.get('driver')) + + def test__parse_driver_info_apc_rackpdu(self): + # Make sure the APC driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='apc_rackpdu') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('apc_rackpdu', info.get('driver')) + + def test__parse_driver_info_aten(self): + # Make sure the Aten driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='aten') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('aten', info.get('driver')) + + def test__parse_driver_info_cyberpower(self): + # Make sure the CyberPower driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='cyberpower') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('cyberpower', info.get('driver')) + + def test__parse_driver_info_eatonpower(self): + # Make sure the Eaton Power driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='eatonpower') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('eatonpower', info.get('driver')) + + def test__parse_driver_info_teltronix(self): + # Make sure the Teltronix driver type is parsed. + info = db_utils.get_test_snmp_info(snmp_driver='teltronix') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('teltronix', info.get('driver')) + + def test__parse_driver_info_snmp_v1(self): + # Make sure SNMPv1 is parsed with a community string. + info = db_utils.get_test_snmp_info(snmp_version='1', + snmp_community='public') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('1', info.get('version')) + self.assertEqual('public', info.get('community')) + + def test__parse_driver_info_snmp_v2c(self): + # Make sure SNMPv2c is parsed with a community string. + info = db_utils.get_test_snmp_info(snmp_version='2c', + snmp_community='private') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('2c', info.get('version')) + self.assertEqual('private', info.get('community')) + + def test__parse_driver_info_snmp_v3(self): + # Make sure SNMPv3 is parsed with a security string. + info = db_utils.get_test_snmp_info(snmp_version='3', + snmp_security='pass') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('3', info.get('version')) + self.assertEqual('pass', info.get('security')) + + def test__parse_driver_info_snmp_port_default(self): + # Make sure default SNMP UDP port numbers are correct + info = dict(INFO_DICT) + del info['snmp_port'] + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual(161, info.get('port')) + + def test__parse_driver_info_snmp_port(self): + # Make sure non-default SNMP UDP port numbers can be configured + info = db_utils.get_test_snmp_info(snmp_port='10161') + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual(10161, info.get('port')) + + def test__parse_driver_info_missing_driver(self): + # Make sure exception is raised when the driver type is missing. + info = dict(INFO_DICT) + del info['snmp_driver'] + node = self._get_test_node(info) + self.assertRaises(exception.MissingParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_invalid_driver(self): + # Make sure exception is raised when the driver type is invalid. + info = db_utils.get_test_snmp_info(snmp_driver='invalidpower') + node = self._get_test_node(info) + self.assertRaises(exception.InvalidParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_missing_address(self): + # Make sure exception is raised when the address is missing. + info = dict(INFO_DICT) + del info['snmp_address'] + node = self._get_test_node(info) + self.assertRaises(exception.MissingParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_missing_outlet(self): + # Make sure exception is raised when the outlet is missing. + info = dict(INFO_DICT) + del info['snmp_outlet'] + node = self._get_test_node(info) + self.assertRaises(exception.MissingParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_default_version(self): + # Make sure version defaults to 1 when it is missing. + info = dict(INFO_DICT) + del info['snmp_version'] + node = self._get_test_node(info) + info = snmp._parse_driver_info(node) + self.assertEqual('1', info.get('version')) + self.assertEqual(INFO_DICT['snmp_community'], info.get('community')) + + def test__parse_driver_info_invalid_version(self): + # Make sure exception is raised when version is invalid. + info = db_utils.get_test_snmp_info(snmp_version='42', + snmp_community='public', + snmp_security='pass') + node = self._get_test_node(info) + self.assertRaises(exception.InvalidParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_default_version_and_missing_community(self): + # Make sure exception is raised when version and community are missing. + info = dict(INFO_DICT) + del info['snmp_version'] + del info['snmp_community'] + node = self._get_test_node(info) + self.assertRaises(exception.MissingParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_missing_community_snmp_v1(self): + # Make sure exception is raised when community is missing with SNMPv1. + info = dict(INFO_DICT) + del info['snmp_community'] + node = self._get_test_node(info) + self.assertRaises(exception.MissingParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_missing_community_snmp_v2c(self): + # Make sure exception is raised when community is missing with SNMPv2c. + info = db_utils.get_test_snmp_info(snmp_version='2c') + del info['snmp_community'] + node = self._get_test_node(info) + self.assertRaises(exception.MissingParameterValue, + snmp._parse_driver_info, + node) + + def test__parse_driver_info_missing_security(self): + # Make sure exception is raised when security is missing with SNMPv3. + info = db_utils.get_test_snmp_info(snmp_version='3') + del info['snmp_security'] + node = self._get_test_node(info) + self.assertRaises(exception.MissingParameterValue, + snmp._parse_driver_info, + node) + + +@mock.patch.object(snmp, '_get_client', autospec=True) +class SNMPDeviceDriverTestCase(db_base.DbTestCase): + """Tests for the SNMP device-specific driver classes. + + The SNMP client object is mocked to allow various error cases to be tested. + """ + + def setUp(self): + super(SNMPDeviceDriverTestCase, self).setUp() + self.node = obj_utils.get_test_node( + self.context, + driver='fake_snmp', + driver_info=INFO_DICT) + + def _update_driver_info(self, **kwargs): + self.node["driver_info"].update(**kwargs) + + def _set_snmp_driver(self, snmp_driver): + self._update_driver_info(snmp_driver=snmp_driver) + + def _get_snmp_failure(self): + return exception.SNMPFailure(operation='test-operation', + error='test-error') + + def test_power_state_on(self, mock_get_client): + # Ensure the power on state is queried correctly + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_on + pstate = driver.power_state() + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_ON, pstate) + + def test_power_state_off(self, mock_get_client): + # Ensure the power off state is queried correctly + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_off + pstate = driver.power_state() + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_OFF, pstate) + + def test_power_state_error(self, mock_get_client): + # Ensure an unexpected power state returns an error + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = 42 + pstate = driver.power_state() + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.ERROR, pstate) + + def test_power_state_snmp_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a query are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = self._get_snmp_failure() + self.assertRaises(exception.SNMPFailure, + driver.power_state) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + + def test_power_on(self, mock_get_client): + # Ensure the device is powered on correctly + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_on + pstate = driver.power_on() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_on) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_ON, pstate) + + def test_power_off(self, mock_get_client): + # Ensure the device is powered off correctly + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_off + pstate = driver.power_off() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_OFF, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_on_delay(self, mock_sleep, mock_get_client): + # Ensure driver waits for the state to change following a power on + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_off, + driver.value_power_on] + pstate = driver.power_on() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_on) + calls = [mock.call(driver._snmp_oid())] * 2 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_off_delay(self, mock_sleep, mock_get_client): + # Ensure driver waits for the state to change following a power off + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_on, + driver.value_power_off] + pstate = driver.power_off() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + calls = [mock.call(driver._snmp_oid())] * 2 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_OFF, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_on_invalid_state(self, mock_sleep, mock_get_client): + # Ensure driver retries when querying unexpected states following a + # power on + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = 42 + pstate = driver.power_on() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_on) + attempts = CONF.snmp.power_timeout // driver.retry_interval + calls = [mock.call(driver._snmp_oid())] * attempts + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_off_invalid_state(self, mock_sleep, mock_get_client): + # Ensure driver retries when querying unexpected states following a + # power off + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = 42 + pstate = driver.power_off() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + attempts = CONF.snmp.power_timeout // driver.retry_interval + calls = [mock.call(driver._snmp_oid())] * attempts + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + def test_power_on_snmp_set_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a power on set operation + # are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.set.side_effect = self._get_snmp_failure() + self.assertRaises(exception.SNMPFailure, + driver.power_on) + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_on) + + def test_power_off_snmp_set_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a power off set + # operation are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.set.side_effect = self._get_snmp_failure() + self.assertRaises(exception.SNMPFailure, + driver.power_off) + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + + def test_power_on_snmp_get_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a power on get operation + # are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = self._get_snmp_failure() + self.assertRaises(exception.SNMPFailure, + driver.power_on) + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_on) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + + def test_power_off_snmp_get_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a power off get + # operation are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = self._get_snmp_failure() + self.assertRaises(exception.SNMPFailure, + driver.power_off) + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_on_timeout(self, mock_sleep, mock_get_client): + # Ensure that a power on consistency poll timeout causes an error + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_off + pstate = driver.power_on() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_on) + attempts = CONF.snmp.power_timeout // driver.retry_interval + calls = [mock.call(driver._snmp_oid())] * attempts + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_off_timeout(self, mock_sleep, mock_get_client): + # Ensure that a power off consistency poll timeout causes an error + mock_client = mock_get_client.return_value + CONF.snmp.power_timeout = 5 + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_on + pstate = driver.power_off() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + attempts = CONF.snmp.power_timeout // driver.retry_interval + calls = [mock.call(driver._snmp_oid())] * attempts + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + def test_power_reset(self, mock_get_client): + # Ensure the device is reset correctly + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_off, + driver.value_power_on] + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid())] * 2 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_reset_off_delay(self, mock_sleep, mock_get_client): + # Ensure driver waits for the power off state change following a power + # reset + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_on, + driver.value_power_off, + driver.value_power_on] + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid())] * 3 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_reset_on_delay(self, mock_sleep, mock_get_client): + # Ensure driver waits for the power on state change following a power + # reset + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_off, + driver.value_power_off, + driver.value_power_on] + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid())] * 3 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_reset_off_delay_on_delay(self, mock_sleep, mock_get_client): + # Ensure driver waits for both state changes following a power reset + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_on, + driver.value_power_off, + driver.value_power_off, + driver.value_power_on] + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid())] * 4 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_ON, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_reset_off_invalid_state(self, mock_sleep, mock_get_client): + # Ensure driver retries when querying unexpected states following a + # power off during a reset + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = 42 + pstate = driver.power_reset() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + attempts = CONF.snmp.power_timeout // driver.retry_interval + calls = [mock.call(driver._snmp_oid())] * attempts + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_reset_on_invalid_state(self, mock_sleep, mock_get_client): + # Ensure driver retries when querying unexpected states following a + # power on during a reset + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + attempts = CONF.snmp.power_timeout // driver.retry_interval + mock_client.get.side_effect = ([driver.value_power_off] + + [42] * attempts) + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid())] * (1 + attempts) + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_reset_off_timeout(self, mock_sleep, mock_get_client): + # Ensure that a power off consistency poll timeout during a reset + # causes an error + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_on + pstate = driver.power_reset() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + attempts = CONF.snmp.power_timeout // driver.retry_interval + calls = [mock.call(driver._snmp_oid())] * attempts + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + @mock.patch("eventlet.greenthread.sleep", autospec=True) + def test_power_reset_on_timeout(self, mock_sleep, mock_get_client): + # Ensure that a power on consistency poll timeout during a reset + # causes an error + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + attempts = CONF.snmp.power_timeout // driver.retry_interval + mock_client.get.side_effect = ([driver.value_power_off] * + (1 + attempts)) + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid())] * (1 + attempts) + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.ERROR, pstate) + + def test_power_reset_off_snmp_set_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a reset power off set + # operation are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.set.side_effect = self._get_snmp_failure() + self.assertRaises(exception.SNMPFailure, + driver.power_reset) + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + self.assertFalse(mock_client.get.called) + + def test_power_reset_off_snmp_get_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a reset power off get + # operation are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = self._get_snmp_failure() + self.assertRaises(exception.SNMPFailure, + driver.power_reset) + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + + def test_power_reset_on_snmp_set_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a reset power on set + # operation are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.set.side_effect = [None, self._get_snmp_failure()] + mock_client.get.return_value = driver.value_power_off + self.assertRaises(exception.SNMPFailure, + driver.power_reset) + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + + def test_power_reset_on_snmp_get_failure(self, mock_get_client): + # Ensure SNMP failure exceptions raised during a reset power on get + # operation are propagated + mock_client = mock_get_client.return_value + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_off, + self._get_snmp_failure()] + self.assertRaises(exception.SNMPFailure, + driver.power_reset) + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid()), mock.call(driver._snmp_oid())] + mock_client.get.assert_has_calls(calls) + + def _test_simple_device_power_state_on(self, snmp_driver, mock_get_client): + # Ensure a simple device driver queries power on correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver(snmp_driver) + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_on + pstate = driver.power_state() + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_ON, pstate) + + def _test_simple_device_power_state_off(self, snmp_driver, + mock_get_client): + # Ensure a simple device driver queries power off correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver(snmp_driver) + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_off + pstate = driver.power_state() + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_OFF, pstate) + + def _test_simple_device_power_on(self, snmp_driver, mock_get_client): + # Ensure a simple device driver powers on correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver(snmp_driver) + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_on + pstate = driver.power_on() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_on) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_ON, pstate) + + def _test_simple_device_power_off(self, snmp_driver, mock_get_client): + # Ensure a simple device driver powers off correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver(snmp_driver) + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.value_power_off + pstate = driver.power_off() + mock_client.set.assert_called_once_with(driver._snmp_oid(), + driver.value_power_off) + mock_client.get.assert_called_once_with(driver._snmp_oid()) + self.assertEqual(states.POWER_OFF, pstate) + + def _test_simple_device_power_reset(self, snmp_driver, mock_get_client): + # Ensure a simple device driver resets correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver(snmp_driver) + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.value_power_off, + driver.value_power_on] + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(), driver.value_power_off), + mock.call(driver._snmp_oid(), driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid())] * 2 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_ON, pstate) + + def test_apc_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the APC + # driver + self._update_driver_info(snmp_driver="apc", + snmp_outlet="3") + driver = snmp._get_driver(self.node) + oid = (1, 3, 6, 1, 4, 1, 318, 1, 1, 4, 4, 2, 1, 3, 3) + self.assertEqual(oid, driver._snmp_oid()) + self.assertEqual(1, driver.value_power_on) + self.assertEqual(2, driver.value_power_off) + + def test_apc_power_state_on(self, mock_get_client): + self._test_simple_device_power_state_on('apc', mock_get_client) + + def test_apc_power_state_off(self, mock_get_client): + self._test_simple_device_power_state_off('apc', mock_get_client) + + def test_apc_power_on(self, mock_get_client): + self._test_simple_device_power_on('apc', mock_get_client) + + def test_apc_power_off(self, mock_get_client): + self._test_simple_device_power_off('apc', mock_get_client) + + def test_apc_power_reset(self, mock_get_client): + self._test_simple_device_power_reset('apc', mock_get_client) + + def test_apc_masterswitch_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the APC + # masterswitch driver + self._update_driver_info(snmp_driver="apc_masterswitch", + snmp_outlet="6") + driver = snmp._get_driver(self.node) + oid = (1, 3, 6, 1, 4, 1, 318, 1, 1, 4, 4, 2, 1, 3, 6) + self.assertEqual(oid, driver._snmp_oid()) + self.assertEqual(1, driver.value_power_on) + self.assertEqual(2, driver.value_power_off) + + def test_apc_masterswitch_power_state_on(self, mock_get_client): + self._test_simple_device_power_state_on('apc_masterswitch', + mock_get_client) + + def test_apc_masterswitch_power_state_off(self, mock_get_client): + self._test_simple_device_power_state_off('apc_masterswitch', + mock_get_client) + + def test_apc_masterswitch_power_on(self, mock_get_client): + self._test_simple_device_power_on('apc_masterswitch', mock_get_client) + + def test_apc_masterswitch_power_off(self, mock_get_client): + self._test_simple_device_power_off('apc_masterswitch', mock_get_client) + + def test_apc_masterswitch_power_reset(self, mock_get_client): + self._test_simple_device_power_reset('apc_masterswitch', + mock_get_client) + + def test_apc_masterswitchplus_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the APC + # masterswitchplus driver + self._update_driver_info(snmp_driver="apc_masterswitchplus", + snmp_outlet="6") + driver = snmp._get_driver(self.node) + oid = (1, 3, 6, 1, 4, 1, 318, 1, 1, 6, 5, 1, 1, 5, 6) + self.assertEqual(oid, driver._snmp_oid()) + self.assertEqual(1, driver.value_power_on) + self.assertEqual(3, driver.value_power_off) + + def test_apc_masterswitchplus_power_state_on(self, mock_get_client): + self._test_simple_device_power_state_on('apc_masterswitchplus', + mock_get_client) + + def test_apc_masterswitchplus_power_state_off(self, mock_get_client): + self._test_simple_device_power_state_off('apc_masterswitchplus', + mock_get_client) + + def test_apc_masterswitchplus_power_on(self, mock_get_client): + self._test_simple_device_power_on('apc_masterswitchplus', + mock_get_client) + + def test_apc_masterswitchplus_power_off(self, mock_get_client): + self._test_simple_device_power_off('apc_masterswitchplus', + mock_get_client) + + def test_apc_masterswitchplus_power_reset(self, mock_get_client): + self._test_simple_device_power_reset('apc_masterswitchplus', + mock_get_client) + + def test_apc_rackpdu_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the APC + # rackpdu driver + self._update_driver_info(snmp_driver="apc_rackpdu", + snmp_outlet="6") + driver = snmp._get_driver(self.node) + oid = (1, 3, 6, 1, 4, 1, 318, 1, 1, 12, 3, 3, 1, 1, 4, 6) + + self.assertEqual(oid, driver._snmp_oid()) + self.assertEqual(1, driver.value_power_on) + self.assertEqual(2, driver.value_power_off) + + def test_apc_rackpdu_power_state_on(self, mock_get_client): + self._test_simple_device_power_state_on('apc_rackpdu', mock_get_client) + + def test_apc_rackpdu_power_state_off(self, mock_get_client): + self._test_simple_device_power_state_off('apc_rackpdu', + mock_get_client) + + def test_apc_rackpdu_power_on(self, mock_get_client): + self._test_simple_device_power_on('apc_rackpdu', mock_get_client) + + def test_apc_rackpdu_power_off(self, mock_get_client): + self._test_simple_device_power_off('apc_rackpdu', mock_get_client) + + def test_apc_rackpdu_power_reset(self, mock_get_client): + self._test_simple_device_power_reset('apc_rackpdu', mock_get_client) + + def test_aten_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the + # Aten driver + self._update_driver_info(snmp_driver="aten", + snmp_outlet="3") + driver = snmp._get_driver(self.node) + oid = (1, 3, 6, 1, 4, 1, 21317, 1, 3, 2, 2, 2, 2, 3, 0) + self.assertEqual(oid, driver._snmp_oid()) + self.assertEqual(2, driver.value_power_on) + self.assertEqual(1, driver.value_power_off) + + def test_aten_power_state_on(self, mock_get_client): + self._test_simple_device_power_state_on('aten', mock_get_client) + + def test_aten_power_state_off(self, mock_get_client): + self._test_simple_device_power_state_off('aten', mock_get_client) + + def test_aten_power_on(self, mock_get_client): + self._test_simple_device_power_on('aten', mock_get_client) + + def test_aten_power_off(self, mock_get_client): + self._test_simple_device_power_off('aten', mock_get_client) + + def test_aten_power_reset(self, mock_get_client): + self._test_simple_device_power_reset('aten', mock_get_client) + + def test_cyberpower_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the + # CyberPower driver + self._update_driver_info(snmp_driver="cyberpower", + snmp_outlet="3") + driver = snmp._get_driver(self.node) + oid = (1, 3, 6, 1, 4, 1, 3808, 1, 1, 3, 3, 3, 1, 1, 4, 3) + self.assertEqual(oid, driver._snmp_oid()) + self.assertEqual(1, driver.value_power_on) + self.assertEqual(2, driver.value_power_off) + + def test_cyberpower_power_state_on(self, mock_get_client): + self._test_simple_device_power_state_on('cyberpower', mock_get_client) + + def test_cyberpower_power_state_off(self, mock_get_client): + self._test_simple_device_power_state_off('cyberpower', mock_get_client) + + def test_cyberpower_power_on(self, mock_get_client): + self._test_simple_device_power_on('cyberpower', mock_get_client) + + def test_cyberpower_power_off(self, mock_get_client): + self._test_simple_device_power_off('cyberpower', mock_get_client) + + def test_cyberpower_power_reset(self, mock_get_client): + self._test_simple_device_power_reset('cyberpower', mock_get_client) + + def test_teltronix_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the + # Teltronix driver + self._update_driver_info(snmp_driver="teltronix", + snmp_outlet="3") + driver = snmp._get_driver(self.node) + oid = (1, 3, 6, 1, 4, 1, 23620, 1, 2, 2, 1, 4, 3) + self.assertEqual(oid, driver._snmp_oid()) + self.assertEqual(2, driver.value_power_on) + self.assertEqual(1, driver.value_power_off) + + def test_teltronix_power_state_on(self, mock_get_client): + self._test_simple_device_power_state_on('teltronix', mock_get_client) + + def test_teltronix_power_state_off(self, mock_get_client): + self._test_simple_device_power_state_off('teltronix', mock_get_client) + + def test_teltronix_power_on(self, mock_get_client): + self._test_simple_device_power_on('teltronix', mock_get_client) + + def test_teltronix_power_off(self, mock_get_client): + self._test_simple_device_power_off('teltronix', mock_get_client) + + def test_teltronix_power_reset(self, mock_get_client): + self._test_simple_device_power_reset('teltronix', mock_get_client) + + def test_eaton_power_snmp_objects(self, mock_get_client): + # Ensure the correct SNMP object OIDs and values are used by the Eaton + # Power driver + self._update_driver_info(snmp_driver="eatonpower", + snmp_outlet="3") + driver = snmp._get_driver(self.node) + status_oid = (1, 3, 6, 1, 4, 1, 534, 6, 6, 7, 6, 6, 1, 2, 3) + poweron_oid = (1, 3, 6, 1, 4, 1, 534, 6, 6, 7, 6, 6, 1, 3, 3) + poweroff_oid = (1, 3, 6, 1, 4, 1, 534, 6, 6, 7, 6, 6, 1, 4, 3) + self.assertEqual(status_oid, driver._snmp_oid(driver.oid_status)) + self.assertEqual(poweron_oid, driver._snmp_oid(driver.oid_poweron)) + self.assertEqual(poweroff_oid, driver._snmp_oid(driver.oid_poweroff)) + self.assertEqual(0, driver.status_off) + self.assertEqual(1, driver.status_on) + self.assertEqual(2, driver.status_pending_off) + self.assertEqual(3, driver.status_pending_on) + + def test_eaton_power_power_state_on(self, mock_get_client): + # Ensure the Eaton Power driver queries on correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver("eatonpower") + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.status_on + pstate = driver.power_state() + mock_client.get.assert_called_once_with( + driver._snmp_oid(driver.oid_status)) + self.assertEqual(states.POWER_ON, pstate) + + def test_eaton_power_power_state_off(self, mock_get_client): + # Ensure the Eaton Power driver queries off correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver("eatonpower") + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.status_off + pstate = driver.power_state() + mock_client.get.assert_called_once_with( + driver._snmp_oid(driver.oid_status)) + self.assertEqual(states.POWER_OFF, pstate) + + def test_eaton_power_power_state_pending_off(self, mock_get_client): + # Ensure the Eaton Power driver queries pending off correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver("eatonpower") + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.status_pending_off + pstate = driver.power_state() + mock_client.get.assert_called_once_with( + driver._snmp_oid(driver.oid_status)) + self.assertEqual(states.POWER_ON, pstate) + + def test_eaton_power_power_state_pending_on(self, mock_get_client): + # Ensure the Eaton Power driver queries pending on correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver("eatonpower") + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.status_pending_on + pstate = driver.power_state() + mock_client.get.assert_called_once_with( + driver._snmp_oid(driver.oid_status)) + self.assertEqual(states.POWER_OFF, pstate) + + def test_eaton_power_power_on(self, mock_get_client): + # Ensure the Eaton Power driver powers on correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver("eatonpower") + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.status_on + pstate = driver.power_on() + mock_client.set.assert_called_once_with( + driver._snmp_oid(driver.oid_poweron), driver.value_power_on) + mock_client.get.assert_called_once_with( + driver._snmp_oid(driver.oid_status)) + self.assertEqual(states.POWER_ON, pstate) + + def test_eaton_power_power_off(self, mock_get_client): + # Ensure the Eaton Power driver powers off correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver("eatonpower") + driver = snmp._get_driver(self.node) + mock_client.get.return_value = driver.status_off + pstate = driver.power_off() + mock_client.set.assert_called_once_with( + driver._snmp_oid(driver.oid_poweroff), driver.value_power_off) + mock_client.get.assert_called_once_with( + driver._snmp_oid(driver.oid_status)) + self.assertEqual(states.POWER_OFF, pstate) + + def test_eaton_power_power_reset(self, mock_get_client): + # Ensure the Eaton Power driver resets correctly + mock_client = mock_get_client.return_value + self._set_snmp_driver("eatonpower") + driver = snmp._get_driver(self.node) + mock_client.get.side_effect = [driver.status_off, driver.status_on] + pstate = driver.power_reset() + calls = [mock.call(driver._snmp_oid(driver.oid_poweroff), + driver.value_power_off), + mock.call(driver._snmp_oid(driver.oid_poweron), + driver.value_power_on)] + mock_client.set.assert_has_calls(calls) + calls = [mock.call(driver._snmp_oid(driver.oid_status))] * 2 + mock_client.get.assert_has_calls(calls) + self.assertEqual(states.POWER_ON, pstate) + + +@mock.patch.object(snmp, '_get_driver', autospec=True) +class SNMPDriverTestCase(db_base.DbTestCase): + """SNMP power driver interface tests. + + In this test case, the SNMP power driver interface is exercised. The + device-specific SNMP driver is mocked to allow various error cases to be + tested. + """ + + def setUp(self): + super(SNMPDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_snmp') + + self.node = obj_utils.create_test_node(self.context, + driver='fake_snmp', + driver_info=INFO_DICT) + + def _get_snmp_failure(self): + return exception.SNMPFailure(operation='test-operation', + error='test-error') + + def test_get_properties(self, mock_get_driver): + expected = snmp.COMMON_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual(expected, task.driver.get_properties()) + + def test_get_power_state_on(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_state.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid) as task: + pstate = task.driver.power.get_power_state(task) + mock_driver.power_state.assert_called_once_with() + self.assertEqual(states.POWER_ON, pstate) + + def test_get_power_state_off(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_state.return_value = states.POWER_OFF + with task_manager.acquire(self.context, self.node.uuid) as task: + pstate = task.driver.power.get_power_state(task) + mock_driver.power_state.assert_called_once_with() + self.assertEqual(states.POWER_OFF, pstate) + + def test_get_power_state_error(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_state.return_value = states.ERROR + with task_manager.acquire(self.context, self.node.uuid) as task: + pstate = task.driver.power.get_power_state(task) + mock_driver.power_state.assert_called_once_with() + self.assertEqual(states.ERROR, pstate) + + def test_get_power_state_snmp_failure(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_state.side_effect = self._get_snmp_failure() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.SNMPFailure, + task.driver.power.get_power_state, task) + mock_driver.power_state.assert_called_once_with() + + def test_set_power_state_on(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_on.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + mock_driver.power_on.assert_called_once_with() + + def test_set_power_state_off(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_off.return_value = states.POWER_OFF + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.set_power_state(task, states.POWER_OFF) + mock_driver.power_off.assert_called_once_with() + + def test_set_power_state_error(self, mock_get_driver): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, states.ERROR) + + def test_set_power_state_on_snmp_failure(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_on.side_effect = self._get_snmp_failure() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.SNMPFailure, + task.driver.power.set_power_state, + task, states.POWER_ON) + mock_driver.power_on.assert_called_once_with() + + def test_set_power_state_off_snmp_failure(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_off.side_effect = self._get_snmp_failure() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.SNMPFailure, + task.driver.power.set_power_state, + task, states.POWER_OFF) + mock_driver.power_off.assert_called_once_with() + + def test_set_power_state_on_timeout(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_on.return_value = states.ERROR + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.set_power_state, + task, states.POWER_ON) + mock_driver.power_on.assert_called_once_with() + + def test_set_power_state_off_timeout(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_off.return_value = states.ERROR + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.set_power_state, + task, states.POWER_OFF) + mock_driver.power_off.assert_called_once_with() + + def test_reboot(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_reset.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.reboot(task) + mock_driver.power_reset.assert_called_once_with() + + def test_reboot_snmp_failure(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_reset.side_effect = self._get_snmp_failure() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.SNMPFailure, + task.driver.power.reboot, task) + mock_driver.power_reset.assert_called_once_with() + + def test_reboot_timeout(self, mock_get_driver): + mock_driver = mock_get_driver.return_value + mock_driver.power_reset.return_value = states.ERROR + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.reboot, task) + mock_driver.power_reset.assert_called_once_with() diff --git a/ironic/tests/unit/drivers/modules/test_ssh.py b/ironic/tests/unit/drivers/modules/test_ssh.py new file mode 100644 index 000000000..a7c25667c --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_ssh.py @@ -0,0 +1,975 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# 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. + +"""Test class for Ironic SSH power driver.""" + +import tempfile + +import mock +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_utils import uuidutils +import paramiko + +from ironic.common import boot_devices +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import states +from ironic.common import utils +from ironic.conductor import task_manager +from ironic.drivers.modules import ssh +from ironic.drivers import utils as driver_utils +from ironic.tests.unit.conductor import utils as mgr_utils +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 + + +CONF = cfg.CONF + + +class SSHValidateParametersTestCase(db_base.DbTestCase): + + def test__parse_driver_info_good_password(self): + # make sure we get back the expected things + node = obj_utils.get_test_node( + self.context, + driver='fake_ssh', + driver_info=db_utils.get_test_ssh_info('password')) + info = ssh._parse_driver_info(node) + self.assertIsNotNone(info.get('host')) + self.assertIsNotNone(info.get('username')) + self.assertIsNotNone(info.get('password')) + self.assertIsNotNone(info.get('port')) + self.assertIsNotNone(info.get('virt_type')) + self.assertIsNotNone(info.get('cmd_set')) + self.assertIsNotNone(info.get('uuid')) + + def test__parse_driver_info_good_key(self): + # make sure we get back the expected things + node = obj_utils.get_test_node( + self.context, + driver='fake_ssh', + driver_info=db_utils.get_test_ssh_info('key')) + info = ssh._parse_driver_info(node) + self.assertIsNotNone(info.get('host')) + self.assertIsNotNone(info.get('username')) + self.assertIsNotNone(info.get('key_contents')) + self.assertIsNotNone(info.get('port')) + self.assertIsNotNone(info.get('virt_type')) + self.assertIsNotNone(info.get('cmd_set')) + self.assertIsNotNone(info.get('uuid')) + + def test__parse_driver_info_good_file(self): + # make sure we get back the expected things + d_info = db_utils.get_test_ssh_info('file') + tempdir = tempfile.mkdtemp() + key_path = tempdir + '/foo' + open(key_path, 'wt').close() + d_info['ssh_key_filename'] = key_path + node = obj_utils.get_test_node( + self.context, + driver='fake_ssh', + driver_info=d_info) + info = ssh._parse_driver_info(node) + self.assertIsNotNone(info.get('host')) + self.assertIsNotNone(info.get('username')) + self.assertIsNotNone(info.get('key_filename')) + self.assertIsNotNone(info.get('port')) + self.assertIsNotNone(info.get('virt_type')) + self.assertIsNotNone(info.get('cmd_set')) + self.assertIsNotNone(info.get('uuid')) + + def test__parse_driver_info_bad_file(self): + # A filename that doesn't exist errors. + info = db_utils.get_test_ssh_info('file') + node = obj_utils.get_test_node( + self.context, + driver='fake_ssh', + driver_info=info) + self.assertRaises( + exception.InvalidParameterValue, ssh._parse_driver_info, node) + + def test__parse_driver_info_too_many(self): + info = db_utils.get_test_ssh_info('too_many') + node = obj_utils.get_test_node( + self.context, + driver='fake_ssh', + driver_info=info) + self.assertRaises( + exception.InvalidParameterValue, ssh._parse_driver_info, node) + + def test__parse_driver_info_missing_host(self): + # make sure error is raised when info is missing + info = db_utils.get_test_ssh_info() + del info['ssh_address'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + ssh._parse_driver_info, + node) + + def test__parse_driver_info_missing_user(self): + # make sure error is raised when info is missing + info = db_utils.get_test_ssh_info() + del info['ssh_username'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + ssh._parse_driver_info, + node) + + def test__parse_driver_info_invalid_creds(self): + # make sure error is raised when info is missing + info = db_utils.get_test_ssh_info('no-creds') + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.InvalidParameterValue, + ssh._parse_driver_info, + node) + + def test__parse_driver_info_missing_virt_type(self): + # make sure error is raised when info is missing + info = db_utils.get_test_ssh_info() + del info['ssh_virt_type'] + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.MissingParameterValue, + ssh._parse_driver_info, + node) + + def test__parse_driver_info_ssh_port_wrong_type(self): + # make sure error is raised when ssh_port is not integer + info = db_utils.get_test_ssh_info() + info['ssh_port'] = 'wrong_port_value' + node = obj_utils.get_test_node(self.context, driver_info=info) + self.assertRaises(exception.InvalidParameterValue, + ssh._parse_driver_info, + node) + + def test__normalize_mac_string(self): + mac_raw = "0A:1B-2C-3D:4F" + mac_clean = ssh._normalize_mac(mac_raw) + self.assertEqual("0a1b2c3d4f", mac_clean) + + def test__normalize_mac_unicode(self): + mac_raw = u"0A:1B-2C-3D:4F" + mac_clean = ssh._normalize_mac(mac_raw) + self.assertEqual("0a1b2c3d4f", mac_clean) + + def test__parse_driver_info_with_custom_libvirt_uri(self): + CONF.set_override('libvirt_uri', 'qemu:///foo', 'ssh') + expected_base_cmd = "LC_ALL=C /usr/bin/virsh --connect qemu:///foo" + + node = obj_utils.get_test_node( + self.context, + driver='fake_ssh', + driver_info=db_utils.get_test_ssh_info()) + node['driver_info']['ssh_virt_type'] = 'virsh' + info = ssh._parse_driver_info(node) + self.assertEqual(expected_base_cmd, info['cmd_set']['base_cmd']) + + def test__get_boot_device_map_parallels(self): + boot_map = ssh._get_boot_device_map('parallels') + self.assertEqual('net0', boot_map[boot_devices.PXE]) + + def test__get_boot_device_map_vbox(self): + boot_map = ssh._get_boot_device_map('vbox') + self.assertEqual('net', boot_map[boot_devices.PXE]) + + def test__get_boot_device_map_exception(self): + self.assertRaises(exception.InvalidParameterValue, + ssh._get_boot_device_map, + 'this_doesn_t_exist') + + +class SSHPrivateMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(SSHPrivateMethodsTestCase, self).setUp() + self.node = obj_utils.get_test_node( + self.context, + driver='fake_ssh', + driver_info=db_utils.get_test_ssh_info()) + self.sshclient = paramiko.SSHClient() + + @mock.patch.object(utils, 'ssh_connect', autospec=True) + def test__get_connection_client(self, ssh_connect_mock): + ssh_connect_mock.return_value = self.sshclient + client = ssh._get_connection(self.node) + self.assertEqual(self.sshclient, client) + driver_info = ssh._parse_driver_info(self.node) + ssh_connect_mock.assert_called_once_with(driver_info) + + @mock.patch.object(utils, 'ssh_connect', autospec=True) + def test__get_connection_exception(self, ssh_connect_mock): + ssh_connect_mock.side_effect = iter( + [exception.SSHConnectFailed(host='fake')]) + self.assertRaises(exception.SSHConnectFailed, + ssh._get_connection, + self.node) + driver_info = ssh._parse_driver_info(self.node) + ssh_connect_mock.assert_called_once_with(driver_info) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + def test__ssh_execute(self, exec_ssh_mock): + ssh_cmd = "somecmd" + expected = ['a', 'b', 'c'] + exec_ssh_mock.return_value = ('\n'.join(expected), '') + lst = ssh._ssh_execute(self.sshclient, ssh_cmd) + exec_ssh_mock.assert_called_once_with(self.sshclient, ssh_cmd) + self.assertEqual(expected, lst) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + def test__ssh_execute_exception(self, exec_ssh_mock): + ssh_cmd = "somecmd" + exec_ssh_mock.side_effect = processutils.ProcessExecutionError + self.assertRaises(exception.SSHCommandFailed, + ssh._ssh_execute, + self.sshclient, + ssh_cmd) + exec_ssh_mock.assert_called_once_with(self.sshclient, ssh_cmd) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__get_power_status_on_unquoted(self, get_hosts_name_mock, + exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + exec_ssh_mock.return_value = ( + 'ExactNodeName', '') + get_hosts_name_mock.return_value = "ExactNodeName" + + pstate = ssh._get_power_status(self.sshclient, info) + + ssh_cmd = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['list_running']) + self.assertEqual(states.POWER_ON, pstate) + exec_ssh_mock.assert_called_once_with(self.sshclient, ssh_cmd) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__get_power_status_on(self, get_hosts_name_mock, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + exec_ssh_mock.return_value = ( + '"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}', '') + get_hosts_name_mock.return_value = "NodeName" + + pstate = ssh._get_power_status(self.sshclient, info) + + ssh_cmd = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['list_running']) + self.assertEqual(states.POWER_ON, pstate) + exec_ssh_mock.assert_called_once_with(self.sshclient, ssh_cmd) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__get_power_status_off(self, get_hosts_name_mock, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + exec_ssh_mock.return_value = ( + '"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}', '') + get_hosts_name_mock.return_value = "NotNodeName" + + pstate = ssh._get_power_status(self.sshclient, info) + + ssh_cmd = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['list_running']) + self.assertEqual(states.POWER_OFF, pstate) + exec_ssh_mock.assert_called_once_with(self.sshclient, ssh_cmd) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__get_power_status_error(self, get_hosts_name_mock, exec_ssh_mock): + + info = ssh._parse_driver_info(self.node) + + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_hosts_name_mock.return_value = None + self.assertRaises(exception.NodeNotFound, + ssh._get_power_status, + self.sshclient, + info) + + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + self.assertFalse(exec_ssh_mock.called) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + def test__get_power_status_exception(self, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + exec_ssh_mock.side_effect = processutils.ProcessExecutionError + + self.assertRaises(exception.SSHCommandFailed, + ssh._get_power_status, + self.sshclient, + info) + ssh_cmd = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['list_all']) + exec_ssh_mock.assert_called_once_with( + self.sshclient, ssh_cmd) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__get_power_status_correct_node(self, get_hosts_name_mock, + exec_ssh_mock): + # Bug: #1397834 test that get_power_status return status of + # baremeta_1 (off) and not baremetal_11 (on) + info = ssh._parse_driver_info(self.node) + exec_ssh_mock.return_value = ('"baremetal_11"\n"seed"\n', '') + get_hosts_name_mock.return_value = "baremetal_1" + + pstate = ssh._get_power_status(self.sshclient, info) + self.assertEqual(states.POWER_OFF, pstate) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + def test__get_hosts_name_for_node_match(self, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + ssh_cmd = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['list_all']) + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['get_node_macs']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + exec_ssh_mock.side_effect = iter([('NodeName', ''), + ('52:54:00:cf:2d:31', '')]) + expected = [mock.call(self.sshclient, ssh_cmd), + mock.call(self.sshclient, cmd_to_exec)] + + found_name = ssh._get_hosts_name_for_node(self.sshclient, info) + + self.assertEqual('NodeName', found_name) + self.assertEqual(expected, exec_ssh_mock.call_args_list) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + def test__get_hosts_name_for_node_no_match(self, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "22:22:22:22:22:22"] + exec_ssh_mock.side_effect = iter([('NodeName', ''), + ('52:54:00:cf:2d:31', '')]) + + ssh_cmd = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['list_all']) + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['get_node_macs']) + + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + expected = [mock.call(self.sshclient, ssh_cmd), + mock.call(self.sshclient, cmd_to_exec)] + + found_name = ssh._get_hosts_name_for_node(self.sshclient, info) + + self.assertIsNone(found_name) + self.assertEqual(expected, exec_ssh_mock.call_args_list) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + def test__get_hosts_name_for_node_exception(self, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + ssh_cmd = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['list_all']) + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['get_node_macs']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + + exec_ssh_mock.side_effect = iter( + [('NodeName', ''), processutils.ProcessExecutionError]) + expected = [mock.call(self.sshclient, ssh_cmd), + mock.call(self.sshclient, cmd_to_exec)] + + self.assertRaises(exception.SSHCommandFailed, + ssh._get_hosts_name_for_node, + self.sshclient, + info) + self.assertEqual(expected, exec_ssh_mock.call_args_list) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__power_on_good(self, get_hosts_name_mock, get_power_status_mock, + exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + + get_power_status_mock.side_effect = iter([states.POWER_OFF, + states.POWER_ON]) + get_hosts_name_mock.return_value = "NodeName" + expected = [mock.call(self.sshclient, info), + mock.call(self.sshclient, info)] + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['start_cmd']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + current_state = ssh._power_on(self.sshclient, info) + + self.assertEqual(states.POWER_ON, current_state) + self.assertEqual(expected, get_power_status_mock.call_args_list) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + exec_ssh_mock.assert_called_once_with(self.sshclient, cmd_to_exec) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__power_on_fail(self, get_hosts_name_mock, get_power_status_mock, + exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_power_status_mock.side_effect = iter([states.POWER_OFF, + states.POWER_OFF]) + get_hosts_name_mock.return_value = "NodeName" + expected = [mock.call(self.sshclient, info), + mock.call(self.sshclient, info)] + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['start_cmd']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + current_state = ssh._power_on(self.sshclient, info) + + self.assertEqual(states.ERROR, current_state) + self.assertEqual(expected, get_power_status_mock.call_args_list) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + exec_ssh_mock.assert_called_once_with(self.sshclient, cmd_to_exec) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__power_on_exception(self, get_hosts_name_mock, + get_power_status_mock, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + + exec_ssh_mock.side_effect = processutils.ProcessExecutionError + get_power_status_mock.side_effect = iter([states.POWER_OFF, + states.POWER_ON]) + get_hosts_name_mock.return_value = "NodeName" + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['start_cmd']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + + self.assertRaises(exception.SSHCommandFailed, + ssh._power_on, + self.sshclient, + info) + get_power_status_mock.assert_called_once_with(self.sshclient, info) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + exec_ssh_mock.assert_called_once_with(self.sshclient, cmd_to_exec) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__power_off_good(self, get_hosts_name_mock, + get_power_status_mock, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_power_status_mock.side_effect = iter([states.POWER_ON, + states.POWER_OFF]) + get_hosts_name_mock.return_value = "NodeName" + expected = [mock.call(self.sshclient, info), + mock.call(self.sshclient, info)] + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['stop_cmd']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + current_state = ssh._power_off(self.sshclient, info) + + self.assertEqual(states.POWER_OFF, current_state) + self.assertEqual(expected, get_power_status_mock.call_args_list) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + exec_ssh_mock.assert_called_once_with(self.sshclient, cmd_to_exec) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__power_off_fail(self, get_hosts_name_mock, + get_power_status_mock, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_power_status_mock.side_effect = iter([states.POWER_ON, + states.POWER_ON]) + get_hosts_name_mock.return_value = "NodeName" + expected = [mock.call(self.sshclient, info), + mock.call(self.sshclient, info)] + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['stop_cmd']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + current_state = ssh._power_off(self.sshclient, info) + + self.assertEqual(states.ERROR, current_state) + self.assertEqual(expected, get_power_status_mock.call_args_list) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + exec_ssh_mock.assert_called_once_with(self.sshclient, cmd_to_exec) + + @mock.patch.object(processutils, 'ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test__power_off_exception(self, get_hosts_name_mock, + get_power_status_mock, exec_ssh_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + exec_ssh_mock.side_effect = processutils.ProcessExecutionError + get_power_status_mock.side_effect = iter([states.POWER_ON, + states.POWER_OFF]) + get_hosts_name_mock.return_value = "NodeName" + + cmd_to_exec = "%s %s" % (info['cmd_set']['base_cmd'], + info['cmd_set']['stop_cmd']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName') + + self.assertRaises(exception.SSHCommandFailed, ssh._power_off, + self.sshclient, info) + get_power_status_mock.assert_called_once_with(self.sshclient, info) + get_hosts_name_mock.assert_called_once_with(self.sshclient, info) + exec_ssh_mock.assert_called_once_with(self.sshclient, cmd_to_exec) + + def test_exec_ssh_command_good(self): + class Channel(object): + def recv_exit_status(self): + return 0 + + class Stream(object): + def __init__(self, buffer=''): + self.buffer = buffer + self.channel = Channel() + + def read(self): + return self.buffer + + def close(self): + pass + + with mock.patch.object(self.sshclient, 'exec_command', + autospec=True) as exec_command_mock: + exec_command_mock.return_value = (Stream(), + Stream('hello'), + Stream()) + stdout, stderr = processutils.ssh_execute(self.sshclient, + "command") + + self.assertEqual('hello', stdout) + exec_command_mock.assert_called_once_with("command") + + def test_exec_ssh_command_fail(self): + class Channel(object): + def recv_exit_status(self): + return 127 + + class Stream(object): + def __init__(self, buffer=''): + self.buffer = buffer + self.channel = Channel() + + def read(self): + return self.buffer + + def close(self): + pass + + with mock.patch.object(self.sshclient, 'exec_command', + autospec=True) as exec_command_mock: + exec_command_mock.return_value = (Stream(), + Stream('hello'), + Stream()) + self.assertRaises(processutils.ProcessExecutionError, + processutils.ssh_execute, + self.sshclient, + "command") + exec_command_mock.assert_called_once_with("command") + + +class SSHDriverTestCase(db_base.DbTestCase): + + def setUp(self): + super(SSHDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ssh") + self.driver = driver_factory.get_driver("fake_ssh") + self.node = obj_utils.create_test_node( + self.context, driver='fake_ssh', + driver_info=db_utils.get_test_ssh_info()) + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + self.sshclient = paramiko.SSHClient() + + @mock.patch.object(utils, 'ssh_connect', autospec=True) + def test__validate_info_ssh_connect_failed(self, ssh_connect_mock): + info = ssh._parse_driver_info(self.node) + + ssh_connect_mock.side_effect = iter( + [exception.SSHConnectFailed(host='fake')]) + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.validate, task) + driver_info = ssh._parse_driver_info(task.node) + ssh_connect_mock.assert_called_once_with(driver_info) + + def test_get_properties(self): + expected = ssh.COMMON_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.power.get_properties()) + self.assertEqual(expected, task.driver.get_properties()) + self.assertEqual(expected, task.driver.management.get_properties()) + + def test_validate_fail_no_port(self): + new_node = obj_utils.create_test_node( + self.context, + uuid='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + driver='fake_ssh', + driver_info=db_utils.get_test_ssh_info()) + with task_manager.acquire(self.context, new_node.uuid, + shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.power.validate, + task) + + @mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_power_on', autospec=True) + def test_reboot_good(self, power_on_mock, get_conn_mock, + get_mac_addr_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_mac_addr_mock.return_value = info['macs'] + get_conn_mock.return_value = self.sshclient + power_on_mock.return_value = states.POWER_ON + with mock.patch.object(ssh, '_parse_driver_info', + autospec=True) as parse_drv_info_mock: + parse_drv_info_mock.return_value = info + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + task.driver.power.reboot(task) + + parse_drv_info_mock.assert_called_once_with(task.node) + get_mac_addr_mock.assert_called_once_with(mock.ANY) + get_conn_mock.assert_called_once_with(task.node) + power_on_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_power_on', autospec=True) + def test_reboot_fail(self, power_on_mock, get_conn_mock, + get_mac_addr_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_mac_addr_mock.return_value = info['macs'] + get_conn_mock.return_value = self.sshclient + power_on_mock.return_value = states.POWER_OFF + with mock.patch.object(ssh, '_parse_driver_info', + autospec=True) as parse_drv_info_mock: + parse_drv_info_mock.return_value = info + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + self.assertRaises(exception.PowerStateFailure, + task.driver.power.reboot, task) + parse_drv_info_mock.assert_called_once_with(task.node) + get_mac_addr_mock.assert_called_once_with(mock.ANY) + get_conn_mock.assert_called_once_with(task.node) + power_on_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True) + @mock.patch.object(ssh, '_get_connection', autospec=True) + def test_set_power_state_bad_state(self, get_conn_mock, + get_mac_addr_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_mac_addr_mock.return_value = info['macs'] + get_conn_mock.return_value = self.sshclient + with mock.patch.object(ssh, '_parse_driver_info', + autospec=True) as parse_drv_info_mock: + parse_drv_info_mock.return_value = info + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + self.assertRaises( + exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, + "BAD_PSTATE") + + parse_drv_info_mock.assert_called_once_with(task.node) + get_mac_addr_mock.assert_called_once_with(mock.ANY) + get_conn_mock.assert_called_once_with(task.node) + + @mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_power_on', autospec=True) + def test_set_power_state_on_good(self, power_on_mock, get_conn_mock, + get_mac_addr_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_mac_addr_mock.return_value = info['macs'] + get_conn_mock.return_value = self.sshclient + power_on_mock.return_value = states.POWER_ON + with mock.patch.object(ssh, '_parse_driver_info', + autospec=True) as parse_drv_info_mock: + parse_drv_info_mock.return_value = info + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + + parse_drv_info_mock.assert_called_once_with(task.node) + get_mac_addr_mock.assert_called_once_with(mock.ANY) + get_conn_mock.assert_called_once_with(task.node) + power_on_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_power_on', autospec=True) + def test_set_power_state_on_fail(self, power_on_mock, get_conn_mock, + get_mac_addr_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_mac_addr_mock.return_value = info['macs'] + get_conn_mock.return_value = self.sshclient + power_on_mock.return_value = states.POWER_OFF + with mock.patch.object(ssh, '_parse_driver_info', + autospec=True) as parse_drv_info_mock: + parse_drv_info_mock.return_value = info + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + self.assertRaises( + exception.PowerStateFailure, + task.driver.power.set_power_state, + task, + states.POWER_ON) + + parse_drv_info_mock.assert_called_once_with(task.node) + get_mac_addr_mock.assert_called_once_with(mock.ANY) + get_conn_mock.assert_called_once_with(task.node) + power_on_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_power_off', autospec=True) + def test_set_power_state_off_good(self, power_off_mock, get_conn_mock, + get_mac_addr_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_mac_addr_mock.return_value = info['macs'] + get_conn_mock.return_value = self.sshclient + power_off_mock.return_value = states.POWER_OFF + with mock.patch.object(ssh, '_parse_driver_info', + autospec=True) as parse_drv_info_mock: + parse_drv_info_mock.return_value = info + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_OFF) + + parse_drv_info_mock.assert_called_once_with(task.node) + get_mac_addr_mock.assert_called_once_with(mock.ANY) + get_conn_mock.assert_called_once_with(task.node) + power_off_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(driver_utils, 'get_node_mac_addresses', autospec=True) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_power_off', autospec=True) + def test_set_power_state_off_fail(self, power_off_mock, get_conn_mock, + get_mac_addr_mock): + info = ssh._parse_driver_info(self.node) + info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"] + get_mac_addr_mock.return_value = info['macs'] + get_conn_mock.return_value = self.sshclient + power_off_mock.return_value = states.POWER_ON + with mock.patch.object(ssh, '_parse_driver_info', + autospec=True) as parse_drv_info_mock: + parse_drv_info_mock.return_value = info + with task_manager.acquire(self.context, info['uuid'], + shared=False) as task: + self.assertRaises( + exception.PowerStateFailure, + task.driver.power.set_power_state, + task, + states.POWER_OFF) + + parse_drv_info_mock.assert_called_once_with(task.node) + get_mac_addr_mock.assert_called_once_with(mock.ANY) + get_conn_mock.assert_called_once_with(task.node) + power_off_mock.assert_called_once_with(self.sshclient, info) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_set_boot_device_vbox_ok(self, mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'vbox' + self.driver.management.set_boot_device(task, boot_devices.PXE) + expected_cmd = ('LC_ALL=C /usr/bin/VBoxManage modifyvm %s ' + '--boot1 net') % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_set_boot_device_parallels_ok(self, mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'parallels' + self.driver.management.set_boot_device(task, boot_devices.PXE) + expected_cmd = ('LC_ALL=C /usr/bin/prlctl set %s ' + '--device-bootorder "net0"') % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_set_boot_device_virsh_ok(self, mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'virsh' + self.driver.management.set_boot_device(task, boot_devices.PXE) + expected_cmd = ('EDITOR="sed -i \'/<boot \\(dev\\|order\\)=*\\>' + '/d;/<\\/os>/i\\<boot dev=\\"network\\"/>\'" ' + 'LC_ALL=C /usr/bin/virsh --connect qemu:///system ' + 'edit %s') % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + def test_set_boot_device_bad_device(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + self.driver.management.set_boot_device, + task, 'invalid-device') + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test_set_boot_device_not_supported(self, mock_h, mock_get_conn): + mock_h.return_value = 'NodeName' + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + # vmware does not support set_boot_device() + task.node['driver_info']['ssh_virt_type'] = 'vmware' + self.assertRaises(NotImplementedError, + self.driver.management.set_boot_device, + task, boot_devices.PXE) + + def test_management_interface_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + expected = [boot_devices.PXE, boot_devices.DISK, + boot_devices.CDROM] + self.assertEqual(sorted(expected), sorted(task.driver.management. + get_supported_boot_devices(task))) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_get_boot_device_vbox(self, mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_exc.return_value = ('net', '') + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'vbox' + result = self.driver.management.get_boot_device(task) + self.assertEqual(boot_devices.PXE, result['boot_device']) + expected_cmd = ('LC_ALL=C /usr/bin/VBoxManage showvminfo ' + '--machinereadable %s ' + '| awk -F \'"\' \'/boot1/{print $2}\'') % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_get_boot_device_parallels(self, mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_exc.return_value = ('net0', '') + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'parallels' + result = self.driver.management.get_boot_device(task) + self.assertEqual(boot_devices.PXE, result['boot_device']) + expected_cmd = ('LC_ALL=C /usr/bin/prlctl list -i %s ' + '| awk \'/^Boot order:/ {print $3}\'') % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_get_boot_device_virsh(self, mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_exc.return_value = ('network', '') + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'virsh' + result = self.driver.management.get_boot_device(task) + self.assertEqual(boot_devices.PXE, result['boot_device']) + expected_cmd = ('LC_ALL=C /usr/bin/virsh --connect ' + 'qemu:///system dumpxml %s | awk \'/boot dev=/ ' + '{ gsub( ".*dev=" Q, "" ); gsub( Q ".*", "" ); ' + 'print; }\' Q="\'" RS="[<>]" | head -1') % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + def test_get_boot_device_not_supported(self, mock_h, mock_get_conn): + mock_h.return_value = 'NodeName' + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + # vmware does not support get_boot_device() + task.node['driver_info']['ssh_virt_type'] = 'vmware' + expected = {'boot_device': None, 'persistent': None} + self.assertEqual(expected, + self.driver.management.get_boot_device(task)) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_get_power_state_vmware(self, mock_exc, mock_h, mock_get_conn): + # To see replacing {_NodeName_} in vmware's list_running + nodename = 'fakevm' + mock_h.return_value = nodename + mock_get_conn.return_value = self.sshclient + # list_running quotes names + mock_exc.return_value = ('"%s"' % nodename, '') + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'vmware' + power_state = self.driver.power.get_power_state(task) + self.assertEqual(states.POWER_ON, power_state) + expected_cmd = ("LC_ALL=C /bin/vim-cmd vmsvc/power.getstate " + "%(node)s | grep 'Powered on' >/dev/null && " + "echo '\"%(node)s\"' || true") % {'node': nodename} + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + def test_management_interface_validate_good(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.management.validate(task) + + def test_management_interface_validate_fail(self): + # Missing SSH driver_info information + node = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + driver='fake_ssh') + with task_manager.acquire(self.context, node.uuid) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.management.validate, task) diff --git a/ironic/tests/unit/drivers/modules/test_virtualbox.py b/ironic/tests/unit/drivers/modules/test_virtualbox.py new file mode 100644 index 000000000..a854cb553 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_virtualbox.py @@ -0,0 +1,374 @@ +# 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. + +"""Test class for VirtualBox Driver Modules.""" + +import mock +from oslo_config import cfg +from pyremotevbox import exception as pyremotevbox_exc +from pyremotevbox import vbox as pyremotevbox_vbox + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import virtualbox +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + +INFO_DICT = { + 'virtualbox_vmname': 'baremetal1', + 'virtualbox_host': '10.0.2.2', + 'virtualbox_username': 'username', + 'virtualbox_password': 'password', + 'virtualbox_port': 12345, +} + +CONF = cfg.CONF + + +class VirtualBoxMethodsTestCase(db_base.DbTestCase): + + def setUp(self): + super(VirtualBoxMethodsTestCase, self).setUp() + driver_info = INFO_DICT.copy() + mgr_utils.mock_the_extension_manager(driver="fake_vbox") + self.node = obj_utils.create_test_node(self.context, + driver='fake_vbox', + driver_info=driver_info) + + def test__parse_driver_info(self): + info = virtualbox._parse_driver_info(self.node) + self.assertEqual('baremetal1', info['vmname']) + self.assertEqual('10.0.2.2', info['host']) + self.assertEqual('username', info['username']) + self.assertEqual('password', info['password']) + self.assertEqual(12345, info['port']) + + def test__parse_driver_info_missing_vmname(self): + del self.node.driver_info['virtualbox_vmname'] + self.assertRaises(exception.MissingParameterValue, + virtualbox._parse_driver_info, self.node) + + def test__parse_driver_info_missing_host(self): + del self.node.driver_info['virtualbox_host'] + self.assertRaises(exception.MissingParameterValue, + virtualbox._parse_driver_info, self.node) + + def test__parse_driver_info_invalid_port(self): + self.node.driver_info['virtualbox_port'] = 'invalid-port' + self.assertRaises(exception.InvalidParameterValue, + virtualbox._parse_driver_info, self.node) + + def test__parse_driver_info_missing_port(self): + del self.node.driver_info['virtualbox_port'] + info = virtualbox._parse_driver_info(self.node) + self.assertEqual(18083, info['port']) + + @mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True) + def test__run_virtualbox_method(self, host_mock): + host_object_mock = mock.MagicMock(spec_set=['find_vm']) + func_mock = mock.MagicMock(spec_set=[]) + vm_object_mock = mock.MagicMock(spec_set=['foo'], foo=func_mock) + host_mock.return_value = host_object_mock + host_object_mock.find_vm.return_value = vm_object_mock + func_mock.return_value = 'return-value' + + return_value = virtualbox._run_virtualbox_method( + self.node, 'some-ironic-method', 'foo', 'args', kwarg='kwarg') + + host_mock.assert_called_once_with(vmname='baremetal1', + host='10.0.2.2', + username='username', + password='password', + port=12345) + host_object_mock.find_vm.assert_called_once_with('baremetal1') + func_mock.assert_called_once_with('args', kwarg='kwarg') + self.assertEqual('return-value', return_value) + + @mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True) + def test__run_virtualbox_method_get_host_fails(self, host_mock): + host_mock.side_effect = pyremotevbox_exc.PyRemoteVBoxException + + self.assertRaises(exception.VirtualBoxOperationFailed, + virtualbox._run_virtualbox_method, + self.node, 'some-ironic-method', 'foo', + 'args', kwarg='kwarg') + + @mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True) + def test__run_virtualbox_method_find_vm_fails(self, host_mock): + host_object_mock = mock.MagicMock(spec_set=['find_vm']) + host_mock.return_value = host_object_mock + exc = pyremotevbox_exc.PyRemoteVBoxException + host_object_mock.find_vm.side_effect = exc + + self.assertRaises(exception.VirtualBoxOperationFailed, + virtualbox._run_virtualbox_method, + self.node, 'some-ironic-method', 'foo', 'args', + kwarg='kwarg') + host_mock.assert_called_once_with(vmname='baremetal1', + host='10.0.2.2', + username='username', + password='password', + port=12345) + host_object_mock.find_vm.assert_called_once_with('baremetal1') + + @mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True) + def test__run_virtualbox_method_func_fails(self, host_mock): + host_object_mock = mock.MagicMock(spec_set=['find_vm']) + host_mock.return_value = host_object_mock + func_mock = mock.MagicMock() + vm_object_mock = mock.MagicMock(spec_set=['foo'], foo=func_mock) + host_object_mock.find_vm.return_value = vm_object_mock + func_mock.side_effect = pyremotevbox_exc.PyRemoteVBoxException + + self.assertRaises(exception.VirtualBoxOperationFailed, + virtualbox._run_virtualbox_method, + self.node, 'some-ironic-method', 'foo', + 'args', kwarg='kwarg') + host_mock.assert_called_once_with(vmname='baremetal1', + host='10.0.2.2', + username='username', + password='password', + port=12345) + host_object_mock.find_vm.assert_called_once_with('baremetal1') + func_mock.assert_called_once_with('args', kwarg='kwarg') + + @mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True) + def test__run_virtualbox_method_invalid_method(self, host_mock): + host_object_mock = mock.MagicMock(spec_set=['find_vm']) + host_mock.return_value = host_object_mock + vm_object_mock = mock.MagicMock(spec_set=[]) + host_object_mock.find_vm.return_value = vm_object_mock + del vm_object_mock.foo + + self.assertRaises(exception.InvalidParameterValue, + virtualbox._run_virtualbox_method, + self.node, 'some-ironic-method', 'foo', + 'args', kwarg='kwarg') + host_mock.assert_called_once_with(vmname='baremetal1', + host='10.0.2.2', + username='username', + password='password', + port=12345) + host_object_mock.find_vm.assert_called_once_with('baremetal1') + + @mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True) + def test__run_virtualbox_method_vm_wrong_power_state(self, host_mock): + host_object_mock = mock.MagicMock(spec_set=['find_vm']) + host_mock.return_value = host_object_mock + func_mock = mock.MagicMock(spec_set=[]) + vm_object_mock = mock.MagicMock(spec_set=['foo'], foo=func_mock) + host_object_mock.find_vm.return_value = vm_object_mock + func_mock.side_effect = pyremotevbox_exc.VmInWrongPowerState + + # _run_virtualbox_method() doesn't catch VmInWrongPowerState and + # lets caller handle it. + self.assertRaises(pyremotevbox_exc.VmInWrongPowerState, + virtualbox._run_virtualbox_method, + self.node, 'some-ironic-method', 'foo', + 'args', kwarg='kwarg') + host_mock.assert_called_once_with(vmname='baremetal1', + host='10.0.2.2', + username='username', + password='password', + port=12345) + host_object_mock.find_vm.assert_called_once_with('baremetal1') + func_mock.assert_called_once_with('args', kwarg='kwarg') + + +class VirtualBoxPowerTestCase(db_base.DbTestCase): + + def setUp(self): + super(VirtualBoxPowerTestCase, self).setUp() + driver_info = INFO_DICT.copy() + mgr_utils.mock_the_extension_manager(driver="fake_vbox") + self.node = obj_utils.create_test_node(self.context, + driver='fake_vbox', + driver_info=driver_info) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + properties = task.driver.power.get_properties() + + self.assertIn('virtualbox_vmname', properties) + self.assertIn('virtualbox_host', properties) + + @mock.patch.object(virtualbox, '_parse_driver_info', autospec=True) + def test_validate(self, parse_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.validate(task) + parse_info_mock.assert_called_once_with(task.node) + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_get_power_state(self, run_method_mock): + run_method_mock.return_value = 'PoweredOff' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + power_state = task.driver.power.get_power_state(task) + run_method_mock.assert_called_once_with(task.node, + 'get_power_state', + 'get_power_status') + self.assertEqual(states.POWER_OFF, power_state) + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_get_power_state_invalid_state(self, run_method_mock): + run_method_mock.return_value = 'invalid-state' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + power_state = task.driver.power.get_power_state(task) + run_method_mock.assert_called_once_with(task.node, + 'get_power_state', + 'get_power_status') + self.assertEqual(states.ERROR, power_state) + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_set_power_state_off(self, run_method_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_OFF) + run_method_mock.assert_called_once_with(task.node, + 'set_power_state', + 'stop') + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_set_power_state_on(self, run_method_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + run_method_mock.assert_called_once_with(task.node, + 'set_power_state', + 'start') + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_set_power_state_reboot(self, run_method_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.set_power_state(task, states.REBOOT) + run_method_mock.assert_any_call(task.node, + 'reboot', + 'stop') + run_method_mock.assert_any_call(task.node, + 'reboot', + 'start') + + def test_set_power_state_invalid_state(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, 'invalid-state') + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_reboot(self, run_method_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.power.reboot(task) + run_method_mock.assert_any_call(task.node, + 'reboot', + 'stop') + run_method_mock.assert_any_call(task.node, + 'reboot', + 'start') + + +class VirtualBoxManagementTestCase(db_base.DbTestCase): + + def setUp(self): + super(VirtualBoxManagementTestCase, self).setUp() + driver_info = INFO_DICT.copy() + mgr_utils.mock_the_extension_manager(driver="fake_vbox") + self.node = obj_utils.create_test_node(self.context, + driver='fake_vbox', + driver_info=driver_info) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + properties = task.driver.management.get_properties() + + self.assertIn('virtualbox_vmname', properties) + self.assertIn('virtualbox_host', properties) + + @mock.patch.object(virtualbox, '_parse_driver_info', autospec=True) + def test_validate(self, parse_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.validate(task) + parse_info_mock.assert_called_once_with(task.node) + + def test_get_supported_boot_devices(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + devices = task.driver.management.get_supported_boot_devices(task) + self.assertIn(boot_devices.PXE, devices) + self.assertIn(boot_devices.DISK, devices) + self.assertIn(boot_devices.CDROM, devices) + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_get_boot_device_ok(self, run_method_mock): + run_method_mock.return_value = 'Network' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ret_val = task.driver.management.get_boot_device(task) + run_method_mock.assert_called_once_with(task.node, + 'get_boot_device', + 'get_boot_device') + self.assertEqual(boot_devices.PXE, ret_val['boot_device']) + self.assertTrue(ret_val['persistent']) + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_get_boot_device_invalid(self, run_method_mock): + run_method_mock.return_value = 'invalid-boot-device' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ret_val = task.driver.management.get_boot_device(task) + self.assertIsNone(ret_val['boot_device']) + self.assertIsNone(ret_val['persistent']) + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_set_boot_device_ok(self, run_method_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, boot_devices.PXE) + run_method_mock.assert_called_once_with(task.node, + 'set_boot_device', + 'set_boot_device', + 'Network') + + @mock.patch.object(virtualbox, 'LOG', autospec=True) + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_set_boot_device_wrong_power_state(self, run_method_mock, + log_mock): + run_method_mock.side_effect = pyremotevbox_exc.VmInWrongPowerState + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, boot_devices.PXE) + log_mock.error.assert_called_once_with(mock.ANY, mock.ANY) + + @mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True) + def test_set_boot_device_invalid(self, run_method_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.management.set_boot_device, + task, 'invalid-boot-device') + + def test_get_sensors_data(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(NotImplementedError, + task.driver.management.get_sensors_data, + task) diff --git a/ironic/tests/unit/drivers/modules/test_wol.py b/ironic/tests/unit/drivers/modules/test_wol.py new file mode 100644 index 000000000..f8e02e298 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_wol.py @@ -0,0 +1,194 @@ +# Copyright 2015 Red Hat, Inc. +# 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. + +"""Test class for Wake-On-Lan driver module.""" + +import socket +import time + +import mock +from oslo_utils import uuidutils + +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import wol +from ironic.tests.unit.conductor import utils as mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + + +@mock.patch.object(time, 'sleep', lambda *_: None) +class WakeOnLanPrivateMethodTestCase(db_base.DbTestCase): + + def setUp(self): + super(WakeOnLanPrivateMethodTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_wol') + self.driver = driver_factory.get_driver('fake_wol') + self.node = obj_utils.create_test_node(self.context, + driver='fake_wol') + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + + def test__parse_parameters(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + params = wol._parse_parameters(task) + self.assertEqual('255.255.255.255', params['host']) + self.assertEqual(9, params['port']) + + def test__parse_parameters_non_default_params(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.driver_info = {'wol_host': '1.2.3.4', + 'wol_port': 7} + params = wol._parse_parameters(task) + self.assertEqual('1.2.3.4', params['host']) + self.assertEqual(7, params['port']) + + def test__parse_parameters_no_ports_fail(self): + node = obj_utils.create_test_node( + self.context, + uuid=uuidutils.generate_uuid(), + driver='fake_wol') + with task_manager.acquire( + self.context, node.uuid, shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + wol._parse_parameters, task) + + @mock.patch.object(socket, 'socket', autospec=True, spec_set=True) + def test_send_magic_packets(self, mock_socket): + fake_socket = mock.Mock(spec=socket, spec_set=True) + mock_socket.return_value = fake_socket() + obj_utils.create_test_port(self.context, + uuid=uuidutils.generate_uuid(), + address='aa:bb:cc:dd:ee:ff', + node_id=self.node.id) + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + wol._send_magic_packets(task, '255.255.255.255', 9) + + expected_calls = [ + mock.call(), + mock.call().setsockopt(socket.SOL_SOCKET, + socket.SO_BROADCAST, 1), + mock.call().sendto(mock.ANY, ('255.255.255.255', 9)), + mock.call().sendto(mock.ANY, ('255.255.255.255', 9)), + mock.call().close()] + + fake_socket.assert_has_calls(expected_calls) + self.assertEqual(1, mock_socket.call_count) + + @mock.patch.object(socket, 'socket', autospec=True, spec_set=True) + def test_send_magic_packets_network_sendto_error(self, mock_socket): + fake_socket = mock.Mock(spec=socket, spec_set=True) + fake_socket.return_value.sendto.side_effect = socket.error('boom') + mock_socket.return_value = fake_socket() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.assertRaises(exception.WolOperationError, + wol._send_magic_packets, + task, '255.255.255.255', 9) + self.assertEqual(1, mock_socket.call_count) + # assert sendt0() was invoked + fake_socket.return_value.sendto.assert_called_once_with( + mock.ANY, ('255.255.255.255', 9)) + + @mock.patch.object(socket, 'socket', autospec=True, spec_set=True) + def test_magic_packet_format(self, mock_socket): + fake_socket = mock.Mock(spec=socket, spec_set=True) + mock_socket.return_value = fake_socket() + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + wol._send_magic_packets(task, '255.255.255.255', 9) + + expct_packet = (b'\xff\xff\xff\xff\xff\xffRT\x00\xcf-1RT\x00' + b'\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT' + b'\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00' + b'\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT' + b'\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1RT\x00\xcf-1') + mock_socket.return_value.sendto.assert_called_once_with( + expct_packet, ('255.255.255.255', 9)) + + +@mock.patch.object(time, 'sleep', lambda *_: None) +class WakeOnLanDriverTestCase(db_base.DbTestCase): + + def setUp(self): + super(WakeOnLanDriverTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_wol') + self.driver = driver_factory.get_driver('fake_wol') + self.node = obj_utils.create_test_node(self.context, + driver='fake_wol') + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + + def test_get_properties(self): + expected = wol.COMMON_PROPERTIES + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + def test_get_power_state(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.power_state = states.POWER_ON + pstate = task.driver.power.get_power_state(task) + self.assertEqual(states.POWER_ON, pstate) + + def test_get_power_state_nostate(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=True) as task: + task.node.power_state = states.NOSTATE + pstate = task.driver.power.get_power_state(task) + self.assertEqual(states.POWER_OFF, pstate) + + @mock.patch.object(wol, '_send_magic_packets', autospec=True, + spec_set=True) + def test_set_power_state_power_on(self, mock_magic): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.set_power_state(task, states.POWER_ON) + mock_magic.assert_called_once_with(task, '255.255.255.255', 9) + + @mock.patch.object(wol.LOG, 'info', autospec=True, spec_set=True) + @mock.patch.object(wol, '_send_magic_packets', autospec=True, + spec_set=True) + def test_set_power_state_power_off(self, mock_magic, mock_log): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.set_power_state(task, states.POWER_OFF) + mock_log.assert_called_once_with(mock.ANY, self.node.uuid) + # assert magic packets weren't sent + self.assertFalse(mock_magic.called) + + @mock.patch.object(wol, '_send_magic_packets', autospec=True, + spec_set=True) + def test_set_power_state_power_fail(self, mock_magic): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.power.set_power_state, + task, 'wrong-state') + # assert magic packets weren't sent + self.assertFalse(mock_magic.called) + + @mock.patch.object(wol.LOG, 'info', autospec=True, spec_set=True) + @mock.patch.object(wol.WakeOnLanPower, 'set_power_state', autospec=True, + spec_set=True) + def test_reboot(self, mock_power, mock_log): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.power.reboot(task) + mock_log.assert_called_once_with(mock.ANY, self.node.uuid) + mock_power.assert_called_once_with(task.driver.power, task, + states.POWER_ON) diff --git a/ironic/tests/unit/drivers/modules/ucs/__init__.py b/ironic/tests/unit/drivers/modules/ucs/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ucs/__init__.py diff --git a/ironic/tests/unit/drivers/modules/ucs/test_helper.py b/ironic/tests/unit/drivers/modules/ucs/test_helper.py new file mode 100644 index 000000000..c83e82d46 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ucs/test_helper.py @@ -0,0 +1,161 @@ +# Copyright 2015, Cisco Systems. + +# 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. + +"""Test class for common methods used by UCS modules.""" + +import mock +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.db import api as dbapi +from ironic.drivers.modules.ucs import helper as ucs_helper +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +ucs_error = importutils.try_import('UcsSdk.utils.exception') + +INFO_DICT = db_utils.get_test_ucs_info() +CONF = cfg.CONF + + +class UcsValidateParametersTestCase(db_base.DbTestCase): + + def setUp(self): + super(UcsValidateParametersTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver="fake_ucs") + self.node = obj_utils.create_test_node(self.context, + driver='fake_ucs', + driver_info=INFO_DICT) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.helper = ucs_helper.CiscoUcsHelper(task) + + def test_parse_driver_info(self): + info = ucs_helper.parse_driver_info(self.node) + + self.assertIsNotNone(info.get('ucs_address')) + self.assertIsNotNone(info.get('ucs_username')) + self.assertIsNotNone(info.get('ucs_password')) + self.assertIsNotNone(info.get('ucs_service_profile')) + + def test_parse_driver_info_missing_address(self): + + del self.node.driver_info['ucs_address'] + self.assertRaises(exception.MissingParameterValue, + ucs_helper.parse_driver_info, self.node) + + def test_parse_driver_info_missing_username(self): + del self.node.driver_info['ucs_username'] + self.assertRaises(exception.MissingParameterValue, + ucs_helper.parse_driver_info, self.node) + + def test_parse_driver_info_missing_password(self): + del self.node.driver_info['ucs_password'] + self.assertRaises(exception.MissingParameterValue, + ucs_helper.parse_driver_info, self.node) + + def test_parse_driver_info_missing_service_profile(self): + del self.node.driver_info['ucs_service_profile'] + self.assertRaises(exception.MissingParameterValue, + ucs_helper.parse_driver_info, self.node) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + def test_connect_ucsm(self, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.helper.connect_ucsm() + + mock_helper.generate_ucsm_handle.assert_called_once_with( + task.node.driver_info['ucs_address'], + task.node.driver_info['ucs_username'], + task.node.driver_info['ucs_password'] + ) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + def test_connect_ucsm_fail(self, mock_helper): + side_effect = ucs_error.UcsConnectionError( + message='connecting to ucsm', + error='failed') + mock_helper.generate_ucsm_handle.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.UcsConnectionError, + self.helper.connect_ucsm + ) + mock_helper.generate_ucsm_handle.assert_called_once_with( + task.node.driver_info['ucs_address'], + task.node.driver_info['ucs_username'], + task.node.driver_info['ucs_password'] + ) + + @mock.patch('ironic.drivers.modules.ucs.helper', + autospec=True) + def test_logout(self, mock_helper): + self.helper.logout() + + +class UcsCommonMethodsTestcase(db_base.DbTestCase): + + def setUp(self): + super(UcsCommonMethodsTestcase, self).setUp() + self.dbapi = dbapi.get_instance() + mgr_utils.mock_the_extension_manager(driver="fake_ucs") + self.node = obj_utils.create_test_node(self.context, + driver='fake_ucs', + driver_info=INFO_DICT.copy()) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.helper = ucs_helper.CiscoUcsHelper(task) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', autospec=True) + @mock.patch('ironic.drivers.modules.ucs.helper.CiscoUcsHelper', + autospec=True) + def test_requires_ucs_client_ok_logout(self, mc_helper, mock_ucs_helper): + mock_helper = mc_helper.return_value + mock_helper.logout.return_value = None + mock_working_function = mock.Mock() + mock_working_function.__name__ = "Working" + mock_working_function.return_valure = "Success" + mock_ucs_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + wont_error = ucs_helper.requires_ucs_client( + mock_working_function) + wont_error(wont_error, task) + mock_helper.logout.assert_called_once_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', autospec=True) + @mock.patch('ironic.drivers.modules.ucs.helper.CiscoUcsHelper', + autospec=True) + def test_requires_ucs_client_fail_logout(self, mc_helper, mock_ucs_helper): + mock_helper = mc_helper.return_value + mock_helper.logout.return_value = None + mock_broken_function = mock.Mock() + mock_broken_function.__name__ = "Broken" + mock_broken_function.side_effect = exception.IronicException() + mock_ucs_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + will_error = ucs_helper.requires_ucs_client(mock_broken_function) + self.assertRaises(exception.IronicException, + will_error, will_error, task) + mock_helper.logout.assert_called_once_with() diff --git a/ironic/tests/unit/drivers/modules/ucs/test_management.py b/ironic/tests/unit/drivers/modules/ucs/test_management.py new file mode 100644 index 000000000..17101756b --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ucs/test_management.py @@ -0,0 +1,139 @@ +# Copyright 2015, Cisco Systems. + +# 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. + +""" +Test class for UCS ManagementInterface +""" + +import mock +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.ucs import helper as ucs_helper +from ironic.drivers.modules.ucs import management as ucs_mgmt +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +ucs_error = importutils.try_import('UcsSdk.utils.exception') + +INFO_DICT = db_utils.get_test_ucs_info() +CONF = cfg.CONF + + +class UcsManagementTestCase(db_base.DbTestCase): + + def setUp(self): + super(UcsManagementTestCase, self).setUp() + mgr_utils.mock_the_extension_manager(driver='fake_ucs') + self.node = obj_utils.create_test_node(self.context, + driver='fake_ucs', + driver_info=INFO_DICT) + self.interface = ucs_mgmt.UcsManagement() + self.task = mock.Mock() + self.task.node = self.node + + def test_get_properties(self): + expected = ucs_helper.COMMON_PROPERTIES + self.assertEqual(expected, self.interface.get_properties()) + + def test_get_supported_boot_devices(self): + expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.CDROM] + self.assertEqual(sorted(expected), + sorted(self.interface.get_supported_boot_devices())) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch( + 'ironic.drivers.modules.ucs.management.ucs_mgmt.BootDeviceHelper', + spec_set=True, autospec=True) + def test_get_boot_device(self, mock_ucs_mgmt, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_mgmt = mock_ucs_mgmt.return_value + mock_mgmt.get_boot_device.return_value = { + 'boot_device': 'disk', + 'persistent': False + } + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_device = boot_devices.DISK + expected_response = {'boot_device': expected_device, + 'persistent': False} + self.assertEqual(expected_response, + self.interface.get_boot_device(task)) + mock_mgmt.get_boot_device.assert_called_once_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch( + 'ironic.drivers.modules.ucs.management.ucs_mgmt.BootDeviceHelper', + spec_set=True, autospec=True) + def test_get_boot_device_fail(self, mock_ucs_mgmt, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_mgmt = mock_ucs_mgmt.return_value + side_effect = ucs_error.UcsOperationError( + operation='getting boot device', + error='failed', + node=self.node.uuid + ) + mock_mgmt.get_boot_device.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.UcsOperationError, + self.interface.get_boot_device, + task) + mock_mgmt.get_boot_device.assert_called_once_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch( + 'ironic.drivers.modules.ucs.management.ucs_mgmt.BootDeviceHelper', + spec_set=True, autospec=True) + def test_set_boot_device(self, mock_mgmt, mock_helper): + mc_mgmt = mock_mgmt.return_value + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.interface.set_boot_device(task, boot_devices.CDROM) + + mc_mgmt.set_boot_device.assert_called_once_with('cdrom', False) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch( + 'ironic.drivers.modules.ucs.management.ucs_mgmt.BootDeviceHelper', + spec_set=True, autospec=True) + def test_set_boot_device_fail(self, mock_mgmt, mock_helper): + mc_mgmt = mock_mgmt.return_value + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + side_effect = exception.UcsOperationError( + operation='setting boot device', + error='failed', + node=self.node.uuid) + mc_mgmt.set_boot_device.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IronicException, + self.interface.set_boot_device, + task, boot_devices.PXE) + mc_mgmt.set_boot_device.assert_called_once_with( + boot_devices.PXE, False) + + def test_get_sensors_data(self): + self.assertRaises(NotImplementedError, + self.interface.get_sensors_data, self.task) diff --git a/ironic/tests/unit/drivers/modules/ucs/test_power.py b/ironic/tests/unit/drivers/modules/ucs/test_power.py new file mode 100644 index 000000000..a01a96bbd --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ucs/test_power.py @@ -0,0 +1,302 @@ +# Copyright 2015, Cisco Systems. + +# 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. + +"""Test class for UcsPower module.""" +import mock +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import exception +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules.ucs import helper as ucs_helper +from ironic.drivers.modules.ucs import power as ucs_power +from ironic.tests.unit.conductor import utils as mgr_utils +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 + +ucs_error = importutils.try_import('UcsSdk.utils.exception') + +INFO_DICT = db_utils.get_test_ucs_info() +CONF = cfg.CONF + + +class UcsPowerTestCase(db_base.DbTestCase): + + def setUp(self): + super(UcsPowerTestCase, self).setUp() + driver_info = INFO_DICT + mgr_utils.mock_the_extension_manager(driver="fake_ucs") + self.node = obj_utils.create_test_node(self.context, + driver='fake_ucs', + driver_info=driver_info) + CONF.set_override('max_retry', 2, 'cisco_ucs') + CONF.set_override('action_interval', 0, 'cisco_ucs') + self.interface = ucs_power.Power() + + def test_get_properties(self): + expected = ucs_helper.COMMON_PROPERTIES + expected.update(ucs_helper.COMMON_PROPERTIES) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(ucs_helper, 'parse_driver_info', + spec_set=True, autospec=True) + def test_validate(self, mock_parse_driver_info): + mock_parse_driver_info.return_value = {} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.interface.validate(task) + mock_parse_driver_info.assert_called_once_with(task.node) + + @mock.patch.object(ucs_helper, 'parse_driver_info', + spec_set=True, autospec=True) + def test_validate_fail(self, mock_parse_driver_info): + side_effect = iter([exception.InvalidParameterValue('Invalid Input')]) + mock_parse_driver_info.side_effect = side_effect + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + self.interface.validate, + task) + mock_parse_driver_info.assert_called_once_with(task.node) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_get_power_state_up(self, mock_power_helper, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_power = mock_power_helper.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_power.get_power_state.return_value = 'up' + self.assertEqual(states.POWER_ON, + self.interface.get_power_state(task)) + mock_power.get_power_state.assert_called_once_with() + mock_power.get_power_state.reset_mock() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_get_power_state_down(self, mock_power_helper, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_power = mock_power_helper.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_power.get_power_state.return_value = 'down' + self.assertEqual(states.POWER_OFF, + self.interface.get_power_state(task)) + mock_power.get_power_state.assert_called_once_with() + mock_power.get_power_state.reset_mock() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_get_power_state_error(self, mock_power_helper, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_power = mock_power_helper.return_value + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_power.get_power_state.return_value = states.ERROR + self.assertEqual(states.ERROR, + self.interface.get_power_state(task)) + mock_power.get_power_state.assert_called_once_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_get_power_state_fail(self, + mock_ucs_power, + mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + power = mock_ucs_power.return_value + power.get_power_state.side_effect = ( + ucs_error.UcsOperationError(operation='getting power state', + error='failed')) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.UcsOperationError, + self.interface.get_power_state, + task) + power.get_power_state.assert_called_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power._wait_for_state_change', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_set_power_state(self, mock_power_helper, mock__wait, mock_helper): + target_state = states.POWER_ON + mock_power = mock_power_helper.return_value + mock_power.get_power_state.side_effect = ['down', 'up'] + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock__wait.return_value = target_state + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertIsNone(self.interface.set_power_state(task, + target_state)) + + mock_power.set_power_state.assert_called_once_with('up') + mock_power.get_power_state.assert_called_once_with() + mock__wait.assert_called_once_with(target_state, mock_power) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_set_power_state_fail(self, mock_power_helper, mock_helper): + mock_power = mock_power_helper.return_value + mock_power.set_power_state.side_effect = ( + ucs_error.UcsOperationError(operation='setting power state', + error='failed')) + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.UcsOperationError, + self.interface.set_power_state, + task, states.POWER_OFF) + mock_power.set_power_state.assert_called_once_with('down') + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + def test_set_power_state_invalid_state(self, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.InvalidParameterValue, + self.interface.set_power_state, + task, states.ERROR) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test__wait_for_state_change_already_target_state( + self, + mock_ucs_power, + mock_helper): + mock_power = mock_ucs_power.return_value + target_state = states.POWER_ON + mock_power.get_power_state.return_value = 'up' + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + self.assertEqual(states.POWER_ON, + ucs_power._wait_for_state_change( + target_state, mock_power)) + mock_power.get_power_state.assert_called_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test__wait_for_state_change_exceed_iterations( + self, + mock_power_helper, + mock_helper): + mock_power = mock_power_helper.return_value + target_state = states.POWER_ON + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_power.get_power_state.side_effect = ( + ['down', 'down', 'down', 'down']) + self.assertEqual(states.ERROR, + ucs_power._wait_for_state_change( + target_state, mock_power) + ) + mock_power.get_power_state.assert_called_with() + self.assertEqual(4, mock_power.get_power_state.call_count) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power._wait_for_state_change', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_set_and_wait_for_state_change_fail( + self, + mock_power_helper, + mock__wait, + mock_helper): + target_state = states.POWER_ON + mock_power = mock_power_helper.return_value + mock_power.get_power_state.return_value = 'down' + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock__wait.return_value = states.POWER_OFF + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.PowerStateFailure, + self.interface.set_power_state, + task, + target_state) + + mock_power.set_power_state.assert_called_once_with('up') + mock_power.get_power_state.assert_called_once_with() + mock__wait.assert_called_once_with(target_state, mock_power) + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power._wait_for_state_change', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_reboot(self, mock_power_helper, mock__wait, mock_helper): + mock_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_power = mock_power_helper.return_value + mock__wait.return_value = states.POWER_ON + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertIsNone(self.interface.reboot(task)) + mock_power.reboot.assert_called_once_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_reboot_fail(self, mock_power_helper, + mock_ucs_helper): + mock_ucs_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_power = mock_power_helper.return_value + mock_power.reboot.side_effect = ( + ucs_error.UcsOperationError(operation='rebooting', error='failed')) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.UcsOperationError, + self.interface.reboot, + task + ) + mock_power.reboot.assert_called_once_with() + + @mock.patch('ironic.drivers.modules.ucs.helper.ucs_helper', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power._wait_for_state_change', + spec_set=True, autospec=True) + @mock.patch('ironic.drivers.modules.ucs.power.ucs_power.UcsPower', + spec_set=True, autospec=True) + def test_reboot__wait_state_change_fail(self, mock_power_helper, + mock__wait, + mock_ucs_helper): + mock_ucs_helper.generate_ucsm_handle.return_value = (True, mock.Mock()) + mock_power = mock_power_helper.return_value + mock__wait.return_value = states.ERROR + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.PowerStateFailure, + self.interface.reboot, + task) + mock_power.reboot.assert_called_once_with() |