From cf02e5161d8547395c40334ac8be43e377472867 Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Wed, 14 Dec 2016 14:32:10 +0000 Subject: [PATCH] Ironic: Add soft power off support to Ironic driver. This patch introduces soft power off support to Ironic virt driver. Change-Id: Ie81d5c6e7f12cc5371ca3475daab09aa7d92d645 Implements: blueprint soft-reboot-poweroff Co-Authored-By: Tang Chen Co-Authored-By: xiexs --- nova/tests/unit/virt/ironic/test_driver.py | 80 ++++++++++++++----- nova/tests/unit/virt/ironic/utils.py | 2 +- nova/virt/ironic/driver.py | 47 +++++++++-- ...soft-reboot-poweroff-6215d216a6aedafa.yaml | 5 ++ 4 files changed, 105 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/bp-soft-reboot-poweroff-6215d216a6aedafa.yaml diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py index 4472f1ee9d7e..4d8c7fe504a7 100644 --- a/nova/tests/unit/virt/ironic/test_driver.py +++ b/nova/tests/unit/virt/ironic/test_driver.py @@ -1372,24 +1372,11 @@ class IronicDriverTestCase(test.NoDBTestCase): mock_sp.assert_has_calls([mock.call(node.uuid, 'reboot', soft=True), mock.call(node.uuid, 'reboot')]) - @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') - @mock.patch.object(ironic_driver.IronicDriver, - '_validate_instance_and_node') - @mock.patch.object(FAKE_CLIENT.node, 'set_power_state') - def test_power_off(self, mock_sp, fake_validate, mock_looping): - self._test_power_on_off(mock_sp, fake_validate, mock_looping, - method_name='power_off') - @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') @mock.patch.object(ironic_driver.IronicDriver, '_validate_instance_and_node') @mock.patch.object(FAKE_CLIENT.node, 'set_power_state') def test_power_on(self, mock_sp, fake_validate, mock_looping): - self._test_power_on_off(mock_sp, fake_validate, mock_looping, - method_name='power_on') - - def _test_power_on_off(self, mock_sp, fake_validate, mock_looping, - method_name=None): node = ironic_utils.get_test_node() fake_validate.side_effect = [node, node] @@ -1397,14 +1384,65 @@ class IronicDriverTestCase(test.NoDBTestCase): mock_looping.return_value = fake_looping_call instance = fake_instance.fake_instance_obj(self.ctx, node=self.instance_uuid) - # Call the method under test here - if method_name == 'power_on': - self.driver.power_on(self.ctx, instance, - utils.get_test_network_info()) - mock_sp.assert_called_once_with(node.uuid, 'on') - elif method_name == 'power_off': - self.driver.power_off(instance) - mock_sp.assert_called_once_with(node.uuid, 'off') + self.driver.power_on(self.ctx, instance, + utils.get_test_network_info()) + mock_sp.assert_called_once_with(node.uuid, 'on') + + @mock.patch.object(loopingcall, 'FixedIntervalLoopingCall') + def _test_power_off(self, mock_looping, timeout=0): + fake_looping_call = FakeLoopingCall() + mock_looping.return_value = fake_looping_call + instance = fake_instance.fake_instance_obj(self.ctx, + node=self.instance_uuid) + self.driver.power_off(instance, timeout) + + @mock.patch.object(ironic_driver.IronicDriver, + '_validate_instance_and_node') + @mock.patch.object(FAKE_CLIENT.node, 'set_power_state') + def test_power_off(self, mock_sp, fake_validate): + node = ironic_utils.get_test_node() + fake_validate.side_effect = [node, node] + + self._test_power_off() + mock_sp.assert_called_once_with(node.uuid, 'off') + + @mock.patch.object(ironic_driver.IronicDriver, + '_validate_instance_and_node') + @mock.patch.object(FAKE_CLIENT.node, 'set_power_state') + def test_power_off_soft(self, mock_sp, fake_validate): + node = ironic_utils.get_test_node() + power_off_node = ironic_utils.get_test_node( + power_state=ironic_states.POWER_OFF) + fake_validate.side_effect = [node, power_off_node] + + self._test_power_off(timeout=30) + mock_sp.assert_called_once_with(node.uuid, 'off', soft=True, + timeout=30) + + @mock.patch.object(ironic_driver.IronicDriver, + '_validate_instance_and_node') + @mock.patch.object(FAKE_CLIENT.node, 'set_power_state') + def test_power_off_soft_exception(self, mock_sp, fake_validate): + node = ironic_utils.get_test_node() + fake_validate.side_effect = [node, node] + mock_sp.side_effect = [ironic_exception.BadRequest(), None] + + self._test_power_off(timeout=30) + mock_sp.assert_has_calls([mock.call(node.uuid, 'off', soft=True, + timeout=30). + mock.call(node.uuid, 'off')]) + + @mock.patch.object(ironic_driver.IronicDriver, + '_validate_instance_and_node') + @mock.patch.object(FAKE_CLIENT.node, 'set_power_state') + def test_power_off_soft_not_stopped(self, mock_sp, fake_validate): + node = ironic_utils.get_test_node() + fake_validate.side_effect = [node, node] + + self._test_power_off(timeout=30) + mock_sp.assert_has_calls([mock.call(node.uuid, 'off', soft=True, + timeout=30). + mock.call(node.uuid, 'off')]) @mock.patch.object(FAKE_CLIENT.node, 'vif_attach') def test_plug_vifs_with_port(self, mock_vatt): diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py index e184d2edc420..ca86dc8e4e40 100644 --- a/nova/tests/unit/virt/ironic/utils.py +++ b/nova/tests/unit/virt/ironic/utils.py @@ -147,7 +147,7 @@ class FakeNodeClient(object): def list_ports(self, node_uuid, detail=False): pass - def set_power_state(self, node_uuid, target, soft=False): + def set_power_state(self, node_uuid, target, soft=False, timeout=None): pass def set_provision_state(self, node_uuid, target): diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py index 280e4e82f9f4..192ed6e9e947 100644 --- a/nova/virt/ironic/driver.py +++ b/nova/virt/ironic/driver.py @@ -1038,25 +1038,58 @@ class IronicDriver(virt_driver.ComputeDriver): def power_off(self, instance, timeout=0, retry_interval=0): """Power off the specified instance. - NOTE: Ironic does not support soft-off, so this method ignores - timeout and retry_interval parameters. NOTE: Unlike the libvirt driver, this method does not delete and recreate the instance; it preserves local state. :param instance: The instance object. - :param timeout: time to wait for node to shutdown. Ignored by - this driver. + :param timeout: time to wait for node to shutdown. If it is set, + soft power off is attempted before hard power off. :param retry_interval: How often to signal node while waiting - for it to shutdown. Ignored by this driver. + for it to shutdown. Ignored by this driver. Retrying depends on + Ironic hardware driver. """ LOG.debug('Power off called for instance', instance=instance) node = self._validate_instance_and_node(instance) - self.ironicclient.call("node.set_power_state", node.uuid, 'off') + if timeout: + try: + self.ironicclient.call("node.set_power_state", node.uuid, + 'off', soft=True, timeout=timeout) + + timer = loopingcall.FixedIntervalLoopingCall( + self._wait_for_power_state, instance, 'soft power off') + timer.start(interval=CONF.ironic.api_retry_interval).wait() + node = self._validate_instance_and_node(instance) + if node.power_state == ironic_states.POWER_OFF: + LOG.info(_LI('Successfully soft powered off Ironic node ' + '%s'), + node.uuid, instance=instance) + return + LOG.info(_LI("Failed to soft power off instance " + "%(instance)s on baremetal node %(node)s " + "within the required timeout %(timeout)d " + "seconds due to error: %(reason)s. " + "Attempting hard power off."), + {'instance': instance.uuid, + 'timeout': timeout, + 'node': node.uuid, + 'reason': node.last_error}, + instance=instance) + except ironic.exc.ClientException as e: + LOG.info(_LI("Failed to soft power off instance " + "%(instance)s on baremetal node %(node)s " + "due to error: %(reason)s. " + "Attempting hard power off."), + {'instance': instance.uuid, + 'node': node.uuid, + 'reason': e}, + instance=instance) + + self.ironicclient.call("node.set_power_state", node.uuid, 'off') timer = loopingcall.FixedIntervalLoopingCall( self._wait_for_power_state, instance, 'power off') timer.start(interval=CONF.ironic.api_retry_interval).wait() - LOG.info(_LI('Successfully powered off Ironic node %s'), + LOG.info(_LI('Successfully hard powered off Ironic node %s'), node.uuid, instance=instance) def power_on(self, context, instance, network_info, diff --git a/releasenotes/notes/bp-soft-reboot-poweroff-6215d216a6aedafa.yaml b/releasenotes/notes/bp-soft-reboot-poweroff-6215d216a6aedafa.yaml new file mode 100644 index 000000000000..487d4beece59 --- /dev/null +++ b/releasenotes/notes/bp-soft-reboot-poweroff-6215d216a6aedafa.yaml @@ -0,0 +1,5 @@ +--- +features: + - Adds soft power off support to Ironic virt driver. This feature requires + the Ironic service to support API version 1.27 or later. It also requires + python-ironicclient >= 1.10.0.