diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py index 71039f1e491a..4472f1ee9d7e 100644 --- a/nova/tests/unit/virt/ironic/test_driver.py +++ b/nova/tests/unit/virt/ironic/test_driver.py @@ -1336,9 +1336,42 @@ class IronicDriverTestCase(test.NoDBTestCase): mock_looping.return_value = fake_looping_call instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid) - self.driver.reboot(self.ctx, instance, None, None) + self.driver.reboot(self.ctx, instance, None, 'HARD') mock_sp.assert_called_once_with(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_reboot_soft(self, mock_sp, fake_validate, mock_looping): + node = ironic_utils.get_test_node() + fake_validate.side_effect = [node, node] + + fake_looping_call = FakeLoopingCall() + mock_looping.return_value = fake_looping_call + instance = fake_instance.fake_instance_obj(self.ctx, + node=node.uuid) + self.driver.reboot(self.ctx, instance, None, 'SOFT') + mock_sp.assert_called_once_with(node.uuid, 'reboot', soft=True) + + @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_reboot_soft_not_supported(self, mock_sp, fake_validate, + mock_looping): + node = ironic_utils.get_test_node() + fake_validate.side_effect = [node, node] + mock_sp.side_effect = [ironic_exception.BadRequest(), None] + + fake_looping_call = FakeLoopingCall() + mock_looping.return_value = fake_looping_call + instance = fake_instance.fake_instance_obj(self.ctx, + node=node.uuid) + self.driver.reboot(self.ctx, instance, None, 'SOFT') + 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') diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py index 83206005540a..e184d2edc420 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): + def set_power_state(self, node_uuid, target, soft=False): pass def set_provision_state(self, node_uuid, target): diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py index c3261bb0a1f5..280e4e82f9f4 100644 --- a/nova/virt/ironic/driver.py +++ b/nova/virt/ironic/driver.py @@ -993,8 +993,6 @@ class IronicDriver(virt_driver.ComputeDriver): block_device_info=None, bad_volumes_callback=None): """Reboot the specified instance. - NOTE: Ironic does not support soft-off, so this method - always performs a hard-reboot. NOTE: Unlike the libvirt driver, this method does not delete and recreate the instance; it preserves local state. @@ -1002,23 +1000,40 @@ class IronicDriver(virt_driver.ComputeDriver): :param instance: The instance object. :param network_info: Instance network information. Ignored by this driver. - :param reboot_type: Either a HARD or SOFT reboot. Ignored by - this driver. + :param reboot_type: Either a HARD or SOFT reboot. :param block_device_info: Info pertaining to attached volumes. Ignored by this driver. :param bad_volumes_callback: Function to handle any bad volumes encountered. Ignored by this driver. """ - LOG.debug('Reboot called for instance', instance=instance) + LOG.debug('Reboot(type %s) called for instance', + reboot_type, instance=instance) node = self._validate_instance_and_node(instance) - self.ironicclient.call("node.set_power_state", node.uuid, 'reboot') + + hard = True + if reboot_type == 'SOFT': + try: + self.ironicclient.call("node.set_power_state", node.uuid, + 'reboot', soft=True) + hard = False + except ironic.exc.BadRequest as exc: + LOG.info(_LI('Soft reboot is not supported by ironic hardware ' + 'driver. Falling back to hard reboot: %s'), + exc, + instance=instance) + + if hard: + self.ironicclient.call("node.set_power_state", node.uuid, 'reboot') timer = loopingcall.FixedIntervalLoopingCall( self._wait_for_power_state, instance, 'reboot') timer.start(interval=CONF.ironic.api_retry_interval).wait() - LOG.info(_LI('Successfully rebooted Ironic node %s'), - node.uuid, instance=instance) + LOG.info(_LI('Successfully rebooted(type %(type)s) Ironic node ' + '%(node)s'), + {'type': ('HARD' if hard else 'SOFT'), + 'node': node.uuid}, + instance=instance) def power_off(self, instance, timeout=0, retry_interval=0): """Power off the specified instance. diff --git a/releasenotes/notes/bp-soft-reboot-poweroff-203e0f33e3b8042e.yaml b/releasenotes/notes/bp-soft-reboot-poweroff-203e0f33e3b8042e.yaml new file mode 100644 index 000000000000..c5c644072224 --- /dev/null +++ b/releasenotes/notes/bp-soft-reboot-poweroff-203e0f33e3b8042e.yaml @@ -0,0 +1,6 @@ +--- +features: + - Adds soft reboot support to Ironic virt driver. If hardware driver in + Ironic doesn't support soft reboot, hard reboot is tried. This feature + requires the Ironic service to support API version 1.27 or later. It also + requires python-ironicclient >= 1.10.0.