PowerVM Driver: spawn/destroy #3: TaskFlow

This change set builds on I85f740999b8d085e803a39c35cc1897c0fb063ad,
introducing the TaskFlow framework for spawn and destroy.  It should be
functionally equivalent to the aforementioned, but sets us up for the
more complex TaskFlow usage in subsequent additions to the PowerVM
driver implementation.

Change-Id: Idfefc2db18d0f473a028b7bb8b593d39067e090d
Partially-Implements: blueprint powervm-nova-compute-driver
This commit is contained in:
Eric Fried 2017-02-27 16:55:28 -06:00
parent 29ef20bf8b
commit 64914fb15e
7 changed files with 319 additions and 9 deletions

View File

@ -0,0 +1,107 @@
# Copyright 2015, 2017 IBM Corp.
#
# 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 pypowervm import const as pvmc
from taskflow import engines as tf_eng
from taskflow.patterns import linear_flow as tf_lf
from taskflow import task as tf_tsk
from nova import exception
from nova import test
from nova.virt.powervm.tasks import vm as tf_vm
class TestVMTasks(test.TestCase):
def setUp(self):
super(TestVMTasks, self).setUp()
self.apt = mock.Mock()
self.instance = mock.Mock()
@mock.patch('pypowervm.tasks.partition.build_active_vio_feed_task',
autospec=True)
@mock.patch('pypowervm.tasks.storage.add_lpar_storage_scrub_tasks',
autospec=True)
@mock.patch('nova.virt.powervm.vm.create_lpar')
def test_create(self, mock_vm_crt, mock_stg, mock_bld):
lpar_entry = mock.Mock()
crt = tf_vm.Create(self.apt, 'host_wrapper', self.instance)
mock_vm_crt.return_value = lpar_entry
crt.execute()
mock_vm_crt.assert_called_once_with(self.apt, 'host_wrapper',
self.instance)
mock_bld.assert_called_once_with(
self.apt, name='create_scrubber',
xag={pvmc.XAG.VIO_SMAP, pvmc.XAG.VIO_FMAP})
mock_stg.assert_called_once_with(
[lpar_entry.id], mock_bld.return_value, lpars_exist=True)
mock_bld.return_value.execute.assert_called_once_with()
@mock.patch('nova.virt.powervm.vm.power_on')
def test_power_on(self, mock_pwron):
pwron = tf_vm.PowerOn(self.apt, self.instance)
pwron.execute()
mock_pwron.assert_called_once_with(self.apt, self.instance)
@mock.patch('nova.virt.powervm.vm.power_on')
@mock.patch('nova.virt.powervm.vm.power_off')
def test_power_on_revert(self, mock_pwroff, mock_pwron):
flow = tf_lf.Flow('revert_power_on')
pwron = tf_vm.PowerOn(self.apt, self.instance)
flow.add(pwron)
# Dummy Task that fails, triggering flow revert
def failure(*a, **k):
raise ValueError()
flow.add(tf_tsk.FunctorTask(failure))
# When PowerOn.execute doesn't fail, revert calls power_off
self.assertRaises(ValueError, tf_eng.run, flow)
mock_pwron.assert_called_once_with(self.apt, self.instance)
mock_pwroff.assert_called_once_with(self.apt, self.instance,
force_immediate=True)
mock_pwron.reset_mock()
mock_pwroff.reset_mock()
# When PowerOn.execute fails, revert doesn't call power_off
mock_pwron.side_effect = exception.NovaException()
self.assertRaises(exception.NovaException, tf_eng.run, flow)
mock_pwron.assert_called_once_with(self.apt, self.instance)
mock_pwroff.assert_not_called()
@mock.patch('nova.virt.powervm.vm.power_off')
def test_power_off(self, mock_pwroff):
# Default force_immediate
pwroff = tf_vm.PowerOff(self.apt, self.instance)
pwroff.execute()
mock_pwroff.assert_called_once_with(self.apt, self.instance,
force_immediate=False)
mock_pwroff.reset_mock()
# Explicit force_immediate
pwroff = tf_vm.PowerOff(self.apt, self.instance, force_immediate=True)
pwroff.execute()
mock_pwroff.assert_called_once_with(self.apt, self.instance,
force_immediate=True)
@mock.patch('nova.virt.powervm.vm.delete_lpar')
def test_delete(self, mock_dlt):
delete = tf_vm.Delete(self.apt, self.instance)
delete.execute()
mock_dlt.assert_called_once_with(self.apt, self.instance)

View File

@ -16,6 +16,7 @@ from __future__ import absolute_import
import fixtures
import mock
from pypowervm import const as pvm_const
from pypowervm import exceptions as pvm_exc
from pypowervm.helpers import log_helper as pvm_hlp_log
from pypowervm.helpers import vios_busy as pvm_hlp_vbusy
@ -87,12 +88,22 @@ class TestPowerVMDriver(test.NoDBTestCase):
self.assertEqual('sys', self.drv.host_wrapper)
@mock.patch('nova.virt.powervm.vm.create_lpar')
@mock.patch('pypowervm.tasks.partition.build_active_vio_feed_task',
autospec=True)
@mock.patch('pypowervm.tasks.storage.add_lpar_storage_scrub_tasks',
autospec=True)
@mock.patch('nova.virt.powervm.vm.power_on')
def test_spawn_ops(self, mock_pwron, mock_crt_lpar):
def test_spawn_ops(self, mock_pwron, mock_scrub, mock_ftsk, mock_crt_lpar):
"""Validates the 'typical' spawn flow of the spawn of an instance. """
self.drv.host_wrapper = 'sys'
self.drv.spawn('context', self.inst, 'img_meta', 'files', 'password')
mock_crt_lpar.assert_called_once_with(self.adp, 'sys', self.inst)
mock_ftsk.assert_called_once_with(
self.adp, name='create_scrubber', xag={pvm_const.XAG.VIO_SMAP,
pvm_const.XAG.VIO_FMAP})
mock_scrub.assert_called_once_with(
[mock_crt_lpar.return_value.id], mock_ftsk.return_value,
lpars_exist=True)
mock_pwron.assert_called_once_with(self.adp, self.inst)
@mock.patch('nova.virt.powervm.vm.delete_lpar')

View File

@ -21,10 +21,13 @@ from pypowervm.helpers import vios_busy as vio_hlp
from pypowervm.tasks import partition as pvm_par
from pypowervm.wrappers import managed_system as pvm_ms
import six
from taskflow.patterns import linear_flow as tf_lf
from nova import exception as exc
from nova.virt import driver
from nova.virt.powervm import host
from nova.virt.powervm.tasks import base as tf_base
from nova.virt.powervm.tasks import vm as tf_vm
from nova.virt.powervm import vm
LOG = logging.getLogger(__name__)
@ -145,14 +148,17 @@ class PowerVMDriver(driver.ComputeDriver):
attached to the instance.
"""
self._log_operation('spawn', instance)
# TODO(efried): Use TaskFlow
vm.create_lpar(self.adapter, self.host_wrapper, instance)
# Define the flow
flow_spawn = tf_lf.Flow("spawn")
flow_spawn.add(tf_vm.Create(self.adapter, self.host_wrapper, instance))
# TODO(thorst, efried) Plug the VIFs
# TODO(thorst, efried) Create/Connect the disk
# TODO(thorst, efried) Add the config drive
# Last step is to power on the system.
vm.power_on(self.adapter, instance)
flow_spawn.add(tf_vm.PowerOn(self.adapter, instance))
# Run the flow.
tf_base.run(flow_spawn, instance=instance)
def destroy(self, context, instance, network_info, block_device_info=None,
destroy_disks=True, migrate_data=None):
@ -172,15 +178,27 @@ class PowerVMDriver(driver.ComputeDriver):
"""
# TODO(thorst, efried) Add resize checks for destroy
self._log_operation('destroy', instance)
try:
# TODO(efried): Use TaskFlow
vm.power_off(self.adapter, instance, force_immediate=destroy_disks)
def _setup_flow_and_run():
# Define the flow
flow = tf_lf.Flow("destroy")
# Power Off the LPAR. If its disks are about to be deleted, issue a
# hard shutdown.
flow.add(tf_vm.PowerOff(self.adapter, instance,
force_immediate=destroy_disks))
# TODO(thorst, efried) Add unplug vifs task
# TODO(thorst, efried) Add config drive tasks
# TODO(thorst, efried) Add volume disconnect tasks
# TODO(thorst, efried) Add disk disconnect/destroy tasks
# TODO(thorst, efried) Add LPAR id based scsi map clean up task
vm.delete_lpar(self.adapter, instance)
flow.add(tf_vm.Delete(self.adapter, instance))
# Build the engine & run!
tf_base.run(flow, instance=instance)
try:
_setup_flow_and_run()
except exc.InstanceNotFound:
LOG.debug('VM was not found during destroy operation.',
instance=instance)

View File

View File

@ -0,0 +1,38 @@
# Copyright 2016, 2017 IBM Corp.
#
# 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 oslo_log import log as logging
from taskflow import engines as tf_eng
from taskflow.listeners import timing as tf_tm
LOG = logging.getLogger(__name__)
def run(flow, instance=None):
"""Run a TaskFlow Flow with task timing and logging with instance.
:param flow: A taskflow.flow.Flow to run.
:param instance: A nova instance, for logging.
:return: The result of taskflow.engines.run(), a dictionary of named
results of the Flow's execution.
"""
def log_with_instance(*args, **kwargs):
"""Wrapper for LOG.info(*args, **kwargs, instance=instance)."""
if instance is not None:
kwargs['instance'] = instance
LOG.info(*args, **kwargs)
eng = tf_eng.load(flow)
with tf_tm.PrintingDurationListener(eng, printer=log_with_instance):
return eng.run()

View File

@ -0,0 +1,136 @@
# Copyright 2015, 2017 IBM Corp.
#
# 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 oslo_log import log as logging
from pypowervm import const as pvm_const
from pypowervm import exceptions as pvm_exc
from pypowervm.tasks import partition as pvm_tpar
from pypowervm.tasks import storage as pvm_stg
from taskflow import task
from taskflow.types import failure as task_fail
from nova.virt.powervm import vm
LOG = logging.getLogger(__name__)
class Create(task.Task):
"""The task for creating a VM."""
def __init__(self, adapter, host_wrapper, instance):
"""Creates the Task for creating a VM.
The revert method only needs to do something for failed rebuilds.
Since the rebuild and build methods have different flows, it is
necessary to clean up the destination LPAR on fails during rebuild.
The revert method is not implemented for build because the compute
manager calls the driver destroy operation for spawn errors. By
not deleting the lpar, it's a cleaner flow through the destroy
operation and accomplishes the same result.
Any stale storage associated with the new VM's (possibly recycled) ID
will be cleaned up. The cleanup work will be delegated to the FeedTask
represented by the stg_ftsk parameter.
:param adapter: The adapter for the pypowervm API
:param host_wrapper: The managed system wrapper
:param instance: The nova instance.
"""
super(Create, self).__init__('crt_vm')
self.instance = instance
self.adapter = adapter
self.host_wrapper = host_wrapper
def execute(self):
wrap = vm.create_lpar(self.adapter, self.host_wrapper, self.instance)
# Get rid of any stale storage and/or mappings associated with the new
# LPAR's ID, so it doesn't accidentally have access to something it
# oughtn't.
ftsk = pvm_tpar.build_active_vio_feed_task(
self.adapter, name='create_scrubber',
xag={pvm_const.XAG.VIO_SMAP, pvm_const.XAG.VIO_FMAP})
pvm_stg.add_lpar_storage_scrub_tasks([wrap.id], ftsk, lpars_exist=True)
LOG.info('Scrubbing stale storage.', instance=self.instance)
ftsk.execute()
class PowerOn(task.Task):
"""The task to power on the instance."""
def __init__(self, adapter, instance):
"""Create the Task for the power on of the LPAR.
:param adapter: The pypowervm adapter.
:param instance: The nova instance.
"""
super(PowerOn, self).__init__('pwr_vm')
self.adapter = adapter
self.instance = instance
def execute(self):
vm.power_on(self.adapter, self.instance)
def revert(self, result, flow_failures):
if isinstance(result, task_fail.Failure):
# The power on itself failed...can't power off.
LOG.debug('Power on failed. Not performing power off.',
instance=self.instance)
return
LOG.warning('Powering off instance.', instance=self.instance)
try:
vm.power_off(self.adapter, self.instance, force_immediate=True)
except pvm_exc.Error:
# Don't raise revert exceptions
LOG.exception("Power-off failed during revert.",
instance=self.instance)
class PowerOff(task.Task):
"""The task to power off a VM."""
def __init__(self, adapter, instance, force_immediate=False):
"""Creates the Task to power off an LPAR.
:param adapter: The adapter for the pypowervm API
:param instance: The nova instance.
:param force_immediate: Boolean. Perform a VSP hard power off.
"""
super(PowerOff, self).__init__('pwr_off_vm')
self.instance = instance
self.adapter = adapter
self.force_immediate = force_immediate
def execute(self):
vm.power_off(self.adapter, self.instance,
force_immediate=self.force_immediate)
class Delete(task.Task):
"""The task to delete the instance from the system."""
def __init__(self, adapter, instance):
"""Create the Task to delete the VM from the system.
:param adapter: The adapter for the pypowervm API.
:param instance: The nova instance.
"""
super(Delete, self).__init__('dlt_vm')
self.adapter = adapter
self.instance = instance
def execute(self):
vm.delete_lpar(self.adapter, self.instance)