diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 56a9df6952..9f7fffebea 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -489,6 +489,15 @@ # value) #protocol=http +# Time interval (in seconds) for successive awake call +# to AMT interface, this depends on the IdleTimeout +# setting on AMT interface. AMT Interface will go to +# sleep after 60 seconds of inactivity by default. +# IdleTimeout=0 means AMT will not go to sleep at all. +# Setting awake_interval=0 will disable awake call. (integer +# value) +#awake_interval=60 + # # Options defined in ironic.drivers.modules.amt.power diff --git a/ironic/common/exception.py b/ironic/common/exception.py index d147f59ce9..bebf500696 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -333,7 +333,8 @@ class IPMIFailure(IronicException): class AMTConnectFailure(IronicException): - _msg_fmt = _("Failed to connect to AMT service.") + _msg_fmt = _("Failed to connect to AMT service. This could be caused " + "by the wrong amt_address or bad network environment.") class AMTFailure(IronicException): diff --git a/ironic/drivers/modules/amt/common.py b/ironic/drivers/modules/amt/common.py index 0ba7facc24..7194bdf20e 100644 --- a/ironic/drivers/modules/amt/common.py +++ b/ironic/drivers/modules/amt/common.py @@ -10,12 +10,13 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ Common functionalities for AMT Driver """ +import time from xml.etree import ElementTree +from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging from oslo_utils import importutils @@ -25,6 +26,7 @@ from ironic.common import boot_devices from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LE +from ironic.common import utils pywsman = importutils.try_import('pywsman') @@ -50,6 +52,15 @@ opts = [ default='http', help=_('Protocol used for AMT endpoint, ' 'support http/https')), + cfg.IntOpt('awake_interval', + default=60, + min=0, + help=_('Time interval (in seconds) for successive awake call ' + 'to AMT interface, this depends on the IdleTimeout ' + 'setting on AMT interface. AMT Interface will go to ' + 'sleep after 60 seconds of inactivity by default. ' + 'IdleTimeout=0 means AMT will not go to sleep at all. ' + 'Setting awake_interval=0 will disable awake call.')), ] CONF = cfg.CONF @@ -75,6 +86,9 @@ AMT_PROTOCOL_PORT_MAP = { # ReturnValue constants RET_SUCCESS = '0' +# A dict cache last awake call to AMT Interface +AMT_AWAKE_CACHE = {} + class Client(object): """AMT client. @@ -206,3 +220,36 @@ def xml_find(doc, namespace, item): query = ('.//{%(namespace)s}%(item)s' % {'namespace': namespace, 'item': item}) return tree.find(query) + + +def awake_amt_interface(node): + """Wake up AMT interface. + + AMT interface goes to sleep after a period of time if the host is off. + This method will ping AMT interface to wake it up. Because there is + no guarantee that the AMT address in driver_info is correct, only + ping the IP five times which is enough to wake it up. + + :param node: an Ironic node object. + :raises: AMTConnectFailure if unable to connect to the server. + """ + awake_interval = CONF.amt.awake_interval + if awake_interval == 0: + return + + now = time.time() + last_awake = AMT_AWAKE_CACHE.get(node.uuid, 0) + if now - last_awake > awake_interval: + cmd_args = ['ping', '-i', 0.2, '-c', 5, + node.driver_info['amt_address']] + try: + utils.execute(*cmd_args) + except processutils.ProcessExecutionError as err: + LOG.error(_LE('Unable to awake AMT interface on node ' + '%(node_id)s. Error: %(error)s'), + {'node_id': node.uuid, 'error': err}) + raise exception.AMTConnectFailure() + else: + LOG.debug(('Successfully awakened AMT interface on node ' + '%(node_id)s.'), {'node_id': node.uuid}) + AMT_AWAKE_CACHE[node.uuid] = now diff --git a/ironic/drivers/modules/amt/management.py b/ironic/drivers/modules/amt/management.py index a648ebbe4c..bcc31b4da7 100644 --- a/ironic/drivers/modules/amt/management.py +++ b/ironic/drivers/modules/amt/management.py @@ -72,6 +72,7 @@ def _set_boot_device_order(node, boot_device): :raises: AMTFailure :raises: AMTConnectFailure """ + amt_common.awake_amt_interface(node) client = amt_common.get_wsman_client(node) device = amt_common.BOOT_DEVICES_MAPPING[boot_device] doc = _generate_change_boot_order_input(device) @@ -129,6 +130,7 @@ def _enable_boot_config(node): :raises: AMTFailure :raises: AMTConnectFailure """ + amt_common.awake_amt_interface(node) client = amt_common.get_wsman_client(node) method = 'SetBootConfigRole' doc = _generate_enable_boot_config_input() diff --git a/ironic/drivers/modules/amt/power.py b/ironic/drivers/modules/amt/power.py index b0eb7af814..d74a95671e 100644 --- a/ironic/drivers/modules/amt/power.py +++ b/ironic/drivers/modules/amt/power.py @@ -97,6 +97,7 @@ def _set_power_state(node, target_state): :raises: AMTFailure :raises: AMTConnectFailure """ + amt_common.awake_amt_interface(node) client = amt_common.get_wsman_client(node) method = 'RequestPowerStateChange' @@ -125,8 +126,8 @@ def _power_status(node): :returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR. :raises: AMTFailure. :raises: AMTConnectFailure. - """ + amt_common.awake_amt_interface(node) client = amt_common.get_wsman_client(node) namespace = resource_uris.CIM_AssociatedPowerManagementService try: diff --git a/ironic/tests/unit/drivers/modules/amt/test_common.py b/ironic/tests/unit/drivers/modules/amt/test_common.py index e19e9c453b..60aa2ce92b 100644 --- a/ironic/tests/unit/drivers/modules/amt/test_common.py +++ b/ironic/tests/unit/drivers/modules/amt/test_common.py @@ -16,9 +16,12 @@ Test class for AMT Common """ import mock +from oslo_concurrency import processutils from oslo_config import cfg +import time from ironic.common import exception +from ironic.common import utils from ironic.drivers.modules.amt import common as amt_common from ironic.drivers.modules.amt import resource_uris from ironic.tests import base @@ -171,3 +174,37 @@ class AMTCommonClientTestCase(base.TestCase): client.wsman_invoke, options, namespace, method) mock_pywsman.invoke.assert_called_once_with(options, namespace, method) + + +class AwakeAMTInterfaceTestCase(db_base.DbTestCase): + def setUp(self): + super(AwakeAMTInterfaceTestCase, self).setUp() + self.info = INFO_DICT + self.node = obj_utils.create_test_node(self.context, + driver='fake_amt', + driver_info=self.info) + + @mock.patch.object(utils, 'execute', spec_set=True, autospec=True) + def test_awake_amt_interface(self, mock_ex): + amt_common.awake_amt_interface(self.node) + expected_args = ['ping', '-i', 0.2, '-c', 5, '1.2.3.4'] + mock_ex.assert_called_once_with(*expected_args) + + @mock.patch.object(utils, 'execute', spec_set=True, autospec=True) + def test_awake_amt_interface_fail(self, mock_ex): + mock_ex.side_effect = processutils.ProcessExecutionError('x') + self.assertRaises(exception.AMTConnectFailure, + amt_common.awake_amt_interface, + self.node) + + @mock.patch.object(utils, 'execute', spec_set=True, autospec=True) + def test_awake_amt_interface_in_cache_time(self, mock_ex): + amt_common.AMT_AWAKE_CACHE[self.node.uuid] = time.time() + amt_common.awake_amt_interface(self.node) + self.assertFalse(mock_ex.called) + + @mock.patch.object(utils, 'execute', spec_set=True, autospec=True) + def test_awake_amt_interface_disable(self, mock_ex): + CONF.set_override('awake_interval', 0, 'amt') + amt_common.awake_amt_interface(self.node) + self.assertFalse(mock_ex.called) diff --git a/ironic/tests/unit/drivers/modules/amt/test_management.py b/ironic/tests/unit/drivers/modules/amt/test_management.py index e879a41067..f79e26bec5 100644 --- a/ironic/tests/unit/drivers/modules/amt/test_management.py +++ b/ironic/tests/unit/drivers/modules/amt/test_management.py @@ -46,7 +46,9 @@ class AMTManagementInteralMethodsTestCase(db_base.DbTestCase): driver='fake_amt', driver_info=INFO_DICT) - def test__set_boot_device_order(self, mock_client_pywsman): + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) + def test__set_boot_device_order(self, mock_aw, mock_client_pywsman): namespace = resource_uris.CIM_BootConfigSetting device = boot_devices.PXE result_xml = test_utils.build_soap_xml([{'ReturnValue': '0'}], @@ -59,8 +61,11 @@ class AMTManagementInteralMethodsTestCase(db_base.DbTestCase): mock_pywsman.invoke.assert_called_once_with( mock.ANY, namespace, 'ChangeBootOrder', mock.ANY) + self.assertTrue(mock_aw.called) - def test__set_boot_device_order_fail(self, mock_client_pywsman): + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) + def test__set_boot_device_order_fail(self, mock_aw, mock_client_pywsman): namespace = resource_uris.CIM_BootConfigSetting device = boot_devices.PXE result_xml = test_utils.build_soap_xml([{'ReturnValue': '2'}], @@ -79,8 +84,11 @@ class AMTManagementInteralMethodsTestCase(db_base.DbTestCase): self.assertRaises(exception.AMTConnectFailure, amt_mgmt._set_boot_device_order, self.node, device) + self.assertTrue(mock_aw.called) - def test__enable_boot_config(self, mock_client_pywsman): + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) + def test__enable_boot_config(self, mock_aw, mock_client_pywsman): namespace = resource_uris.CIM_BootService result_xml = test_utils.build_soap_xml([{'ReturnValue': '0'}], namespace) @@ -92,8 +100,11 @@ class AMTManagementInteralMethodsTestCase(db_base.DbTestCase): mock_pywsman.invoke.assert_called_once_with( mock.ANY, namespace, 'SetBootConfigRole', mock.ANY) + self.assertTrue(mock_aw.called) - def test__enable_boot_config_fail(self, mock_client_pywsman): + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) + def test__enable_boot_config_fail(self, mock_aw, mock_client_pywsman): namespace = resource_uris.CIM_BootService result_xml = test_utils.build_soap_xml([{'ReturnValue': '2'}], namespace) @@ -111,6 +122,7 @@ class AMTManagementInteralMethodsTestCase(db_base.DbTestCase): self.assertRaises(exception.AMTConnectFailure, amt_mgmt._enable_boot_config, self.node) + self.assertTrue(mock_aw.called) class AMTManagementTestCase(db_base.DbTestCase): diff --git a/ironic/tests/unit/drivers/modules/amt/test_power.py b/ironic/tests/unit/drivers/modules/amt/test_power.py index 62790b1bd3..f509bfd442 100644 --- a/ironic/tests/unit/drivers/modules/amt/test_power.py +++ b/ironic/tests/unit/drivers/modules/amt/test_power.py @@ -48,27 +48,35 @@ class AMTPowerInteralMethodsTestCase(db_base.DbTestCase): CONF.set_override('max_attempts', 2, 'amt') CONF.set_override('action_wait', 0, 'amt') + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, autospec=True) - def test__set_power_state(self, mock_client_pywsman): + def test__set_power_state(self, mock_client_pywsman, mock_aw): 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) + self.assertTrue(mock_aw.called) + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, autospec=True) - def test__set_power_state_fail(self, mock_client_pywsman): + def test__set_power_state_fail(self, mock_client_pywsman, mock_aw): 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) + self.assertTrue(mock_aw.called) + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, autospec=True) - def test__power_status(self, mock_gwc): + def test__power_status(self, mock_gwc, mock_aw): namespace = resource_uris.CIM_AssociatedPowerManagementService result_xml = test_utils.build_soap_xml([{'PowerState': '2'}], @@ -96,15 +104,19 @@ class AMTPowerInteralMethodsTestCase(db_base.DbTestCase): mock_client.wsman_get.return_value = mock_doc self.assertEqual( states.ERROR, amt_power._power_status(self.node)) + self.assertTrue(mock_aw.called) + @mock.patch.object(amt_common, 'awake_amt_interface', spec_set=True, + autospec=True) @mock.patch.object(amt_common, 'get_wsman_client', spec_set=True, autospec=True) - def test__power_status_fail(self, mock_gwc): + def test__power_status_fail(self, mock_gwc, mock_aw): 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) + self.assertTrue(mock_aw.called) @mock.patch.object(amt_mgmt.AMTManagement, 'ensure_next_boot_device', spec_set=True, autospec=True)