diff --git a/nova/tests/unit/virt/powervm/test_driver.py b/nova/tests/unit/virt/powervm/test_driver.py index 253ce6f65f15..6fca9ddc2491 100644 --- a/nova/tests/unit/virt/powervm/test_driver.py +++ b/nova/tests/unit/virt/powervm/test_driver.py @@ -134,3 +134,33 @@ class TestPowerVMDriver(test.NoDBTestCase): self.assertRaises(ValueError, self.drv.destroy, 'context', self.inst, [], block_device_info={}) + + @mock.patch('nova.virt.powervm.vm.power_on') + def test_power_on(self, mock_power_on): + self.drv.power_on('context', self.inst, 'network_info') + mock_power_on.assert_called_once_with(self.adp, self.inst) + + @mock.patch('nova.virt.powervm.vm.power_off') + def test_power_off(self, mock_power_off): + self.drv.power_off(self.inst) + mock_power_off.assert_called_once_with( + self.adp, self.inst, force_immediate=True, timeout=None) + + @mock.patch('nova.virt.powervm.vm.power_off') + def test_power_off_timeout(self, mock_power_off): + # Long timeout (retry interval means nothing on powervm) + self.drv.power_off(self.inst, timeout=500, retry_interval=10) + mock_power_off.assert_called_once_with( + self.adp, self.inst, force_immediate=False, timeout=500) + + @mock.patch('nova.virt.powervm.vm.reboot') + def test_reboot_soft(self, mock_reboot): + inst = mock.Mock() + self.drv.reboot('context', inst, 'network_info', 'SOFT') + mock_reboot.assert_called_once_with(self.adp, inst, False) + + @mock.patch('nova.virt.powervm.vm.reboot') + def test_reboot_hard(self, mock_reboot): + inst = mock.Mock() + self.drv.reboot('context', inst, 'network_info', 'HARD') + mock_reboot.assert_called_once_with(self.adp, inst, True) diff --git a/nova/tests/unit/virt/powervm/test_vm.py b/nova/tests/unit/virt/powervm/test_vm.py index ec75dd6dd49c..135101d75dd9 100644 --- a/nova/tests/unit/virt/powervm/test_vm.py +++ b/nova/tests/unit/virt/powervm/test_vm.py @@ -308,9 +308,9 @@ class TestVM(test.NoDBTestCase): mock_lock.assert_called_once_with('power_%s' % self.inst.uuid) mock_power_off.reset_mock() mock_lock.reset_mock() - vm.power_off(None, self.inst, force_immediate=True) - mock_power_off.assert_called_once_with(entry, None, - force_immediate=True) + vm.power_off(None, self.inst, force_immediate=True, timeout=5) + mock_power_off.assert_called_once_with( + entry, None, force_immediate=True, timeout=5) mock_lock.assert_called_once_with('power_%s' % self.inst.uuid) @mock.patch('pypowervm.tasks.power.power_off', autospec=True) @@ -330,6 +330,41 @@ class TestVM(test.NoDBTestCase): mock_power_off.side_effect = ValueError() self.assertRaises(ValueError, vm.power_off, None, self.inst) + @mock.patch('pypowervm.tasks.power.power_on', autospec=True) + @mock.patch('pypowervm.tasks.power.power_off', autospec=True) + @mock.patch('oslo_concurrency.lockutils.lock', autospec=True) + @mock.patch('nova.virt.powervm.vm.get_instance_wrapper') + def test_reboot(self, mock_wrap, mock_lock, mock_pwroff, mock_pwron): + entry = mock.Mock(state=pvm_bp.LPARState.NOT_ACTIVATED) + mock_wrap.return_value = entry + + # No power_off + vm.reboot('adap', self.inst, False) + mock_lock.assert_called_once_with('power_%s' % self.inst.uuid) + mock_wrap.assert_called_once_with('adap', self.inst) + mock_pwron.assert_called_once_with(entry, None) + self.assertEqual(0, mock_pwroff.call_count) + + mock_pwron.reset_mock() + + # power_off (no power_on) + entry.state = pvm_bp.LPARState.RUNNING + for force in (False, True): + mock_pwroff.reset_mock() + vm.reboot('adap', self.inst, force) + self.assertEqual(0, mock_pwron.call_count) + mock_pwroff.assert_called_once_with( + entry, None, force_immediate=force, restart=True) + + # PowerVM error is converted + mock_pwroff.side_effect = pvm_exc.TimeoutError("Timed out") + self.assertRaises(exception.InstanceRebootFailure, + vm.reboot, 'adap', self.inst, True) + + # Non-PowerVM error is raised directly + mock_pwroff.side_effect = ValueError + self.assertRaises(ValueError, vm.reboot, 'adap', self.inst, True) + @mock.patch('oslo_serialization.jsonutils.loads') def test_get_vm_qp(self, mock_loads): self.apt.helpers = ['helper1', pvm_log.log_helper, 'helper3'] diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py index 730fa4b9b5dc..7e1cd004f60f 100644 --- a/nova/virt/powervm/driver.py +++ b/nova/virt/powervm/driver.py @@ -189,3 +189,49 @@ class PowerVMDriver(driver.ComputeDriver): LOG.exception("PowerVM error during destroy.", instance=instance) # Convert to a Nova exception raise exc.InstanceTerminationFailure(reason=six.text_type(e)) + + def power_off(self, instance, timeout=0, retry_interval=0): + """Power off the specified instance. + + :param instance: nova.objects.instance.Instance + :param timeout: time to wait for GuestOS to shutdown + :param retry_interval: How often to signal guest while + waiting for it to shutdown + """ + self._log_operation('power_off', instance) + force_immediate = (timeout == 0) + timeout = timeout or None + vm.power_off(self.adapter, instance, force_immediate=force_immediate, + timeout=timeout) + + def power_on(self, context, instance, network_info, + block_device_info=None): + """Power on the specified instance. + + :param instance: nova.objects.instance.Instance + """ + self._log_operation('power_on', instance) + vm.power_on(self.adapter, instance) + + def reboot(self, context, instance, network_info, reboot_type, + block_device_info=None, bad_volumes_callback=None): + """Reboot the specified instance. + + After this is called successfully, the instance's state + goes back to power_state.RUNNING. The virtualization + platform should ensure that the reboot action has completed + successfully even in cases in which the underlying domain/vm + is paused or halted/stopped. + + :param instance: nova.objects.instance.Instance + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param reboot_type: Either a HARD or SOFT reboot + :param block_device_info: Info pertaining to attached volumes + :param bad_volumes_callback: Function to handle any bad volumes + encountered + """ + self._log_operation(reboot_type + ' reboot', instance) + vm.reboot(self.adapter, instance, reboot_type == 'HARD') + # pypowervm exceptions are sufficient to indicate real failure. + # Otherwise, pypowervm thinks the instance is up. diff --git a/nova/virt/powervm/vm.py b/nova/virt/powervm/vm.py index c4f61ab02c6c..fd0141be94b9 100644 --- a/nova/virt/powervm/vm.py +++ b/nova/virt/powervm/vm.py @@ -128,12 +128,15 @@ def power_on(adapter, instance): raise exc.InstancePowerOnFailure(reason=six.text_type(e)) -def power_off(adapter, instance, force_immediate=False): +def power_off(adapter, instance, force_immediate=False, timeout=None): """Powers off a VM. :param adapter: A pypowervm.adapter.Adapter. :param instance: The nova instance to power off. - :param force_immediate: (Optional, Default False) Should it be immediately + :param timeout: (Optional, Default None) How long to wait for the job + to complete. By default, is None which indicates it should + use the default from pypowervm's power off method. + :param force_immediate: (Optional, Default False) Should it be immediately shut down. :raises: InstancePowerOffFailure """ @@ -150,9 +153,10 @@ def power_off(adapter, instance, force_immediate=False): try: LOG.debug("Power off executing for instance %(inst)s.", {'inst': instance.name}) - force_flag = (power.Force.TRUE if force_immediate - else power.Force.ON_FAILURE) - power.power_off(entry, None, force_immediate=force_flag) + kwargs = {'timeout': timeout} if timeout else {} + force = (power.Force.TRUE if force_immediate + else power.Force.ON_FAILURE) + power.power_off(entry, None, force_immediate=force, **kwargs) except pvm_exc.Error as e: LOG.exception("PowerVM error during power_off.", instance=instance) @@ -162,6 +166,32 @@ def power_off(adapter, instance, force_immediate=False): {'inst': instance.name}) +def reboot(adapter, instance, hard): + """Reboots a VM. + + :param adapter: A pypowervm.adapter.Adapter. + :param instance: The nova instance to reboot. + :param hard: Boolean True if hard reboot, False otherwise. + :raises: InstanceRebootFailure + """ + # Synchronize power-on and power-off ops on a given instance + with lockutils.lock('power_%s' % instance.uuid): + try: + entry = get_instance_wrapper(adapter, instance) + if entry.state != pvm_bp.LPARState.NOT_ACTIVATED: + power.power_off(entry, None, force_immediate=hard, + restart=True) + else: + # pypowervm does NOT throw an exception if "already down". + # Any other exception from pypowervm is a legitimate failure; + # let it raise up. + # If we get here, pypowervm thinks the instance is down. + power.power_on(entry, None) + except pvm_exc.Error as e: + LOG.exception("PowerVM error during reboot.", instance=instance) + raise exc.InstanceRebootFailure(reason=six.text_type(e)) + + def delete_lpar(adapter, instance): """Delete an LPAR.