Convert spawn over to TaskFlow

Initial migration of the current spawn method over to a TaskFlow based
infrastructure.  Handles failure mid way through the spawn flow and
should roll back properly now.

Does not include media updates or any other migrations to TaskFlow.

Change-Id: I990008542c40ac8d10fc9ba698623e696f134b2d
This commit is contained in:
Drew Thorstensen 2015-01-16 15:41:12 -06:00
parent 02b8d18b0c
commit 902c2585e1
8 changed files with 240 additions and 29 deletions

View File

@ -19,6 +19,7 @@ import logging
import mock
from nova import exception as exc
from nova import test
from nova.virt import fake
from pypowervm.tests.wrappers.util import pvmhttp
@ -135,17 +136,64 @@ class TestPowerVMDriver(test.TestCase):
drv.init_host('FakeHost')
drv.adapter = mock_apt
# spawn()
# Set up the mocks to the tasks.
inst = FakeInstance()
my_flavor = FakeFlavor()
mock_get_flv.return_value = my_flavor
mock_crt.return_value = mock.MagicMock()
# Invoke the method.
drv.spawn('context', inst, mock.Mock(),
'injected_files', 'admin_password')
# Create LPAR was called
mock_crt.assert_called_with(mock_apt, drv.host_uuid,
inst, my_flavor)
# Power on was called
self.assertEqual(True, mock_pwron.called)
self.assertTrue(mock_pwron.called)
@mock.patch('pypowervm.adapter.Session')
@mock.patch('pypowervm.adapter.Adapter')
@mock.patch('nova_powervm.virt.powervm.host.find_entry_by_mtm_serial')
@mock.patch('nova_powervm.virt.powervm.vm.crt_lpar')
@mock.patch('nova_powervm.virt.powervm.vm.dlt_lpar')
@mock.patch('nova_powervm.virt.powervm.vm.UUIDCache')
@mock.patch('nova.context.get_admin_context')
@mock.patch('nova.objects.flavor.Flavor.get_by_id')
@mock.patch('nova_powervm.virt.powervm.localdisk.LocalStorage')
@mock.patch('pypowervm.jobs.power.power_on')
@mock.patch('pypowervm.jobs.power.power_off')
def test_spawn_ops_rollback(self, mock_pwroff, mock_pwron, mock_disk,
mock_get_flv, mock_get_ctx, mock_uuidcache,
mock_dlt, mock_crt, mock_find, mock_apt,
mock_sess):
"""Validates the PowerVM driver operations. Will do a rollback."""
drv = driver.PowerVMDriver(fake.FakeVirtAPI())
drv.init_host('FakeHost')
drv.adapter = mock_apt
# Set up the mocks to the tasks.
inst = FakeInstance()
my_flavor = FakeFlavor()
mock_get_flv.return_value = my_flavor
mock_crt.return_value = mock.MagicMock()
# Make sure power on fails.
mock_pwron.side_effect = exc.Forbidden()
# Invoke the method.
self.assertRaises(exc.Forbidden, drv.spawn, 'context', inst,
mock.Mock(), 'injected_files', 'admin_password')
# Create LPAR was called
mock_crt.assert_called_with(mock_apt, drv.host_uuid,
inst, my_flavor)
# Power on was called
self.assertTrue(mock_pwron.called)
# Validate the rollbacks were called
self.assertTrue(mock_pwroff.called)
self.assertTrue(mock_dlt.called)
@mock.patch('nova_powervm.virt.powervm.driver.LOG')
def test_log_op(self, mock_log):

View File

@ -48,5 +48,6 @@ class StorageAdapter(object):
"""
pass
def connect_volume(self, context, instance, volume, **kwds):
def connect_volume(self, context, instance, volume_info, lpar_uuid,
**kwds):
pass

View File

@ -23,6 +23,8 @@ from nova.openstack.common import log as logging
from nova.virt import driver
from oslo.config import cfg
import taskflow.engines
from taskflow.patterns import linear_flow as lf
from pypowervm import adapter as pvm_apt
from pypowervm.helpers import log_helper as log_hlp
@ -32,6 +34,7 @@ from pypowervm.wrappers import managed_system as msentry_wrapper
from nova_powervm.virt.powervm import host as pvm_host
from nova_powervm.virt.powervm import localdisk as blk_lcl
from nova_powervm.virt.powervm.tasks import spawn as tf_spawn
from nova_powervm.virt.powervm import vios
from nova_powervm.virt.powervm import vm
@ -159,26 +162,30 @@ class PowerVMDriver(driver.ComputeDriver):
flavor_obj.Flavor.get_by_id(admin_ctx,
instance.instance_type_id))
# Create the lpar on the host
LOG.info(_LI('Creating instance: %s') % instance.name)
vm.crt_lpar(self.adapter, self.host_uuid, instance, flavor)
# Create the volume on the VIOS
LOG.info(_LI('Creating disk for instance: %s') % instance.name)
vol_info = self.block_dvr.create_volume_from_image(context, instance,
image_meta)
# Attach the boot volume to the instance
LOG.info(_LI('Connecting boot disk to instance: %s') % instance.name)
self.block_dvr.connect_volume(context, instance, vol_info,
pvm_uuids=self.pvm_uuids)
LOG.info(_LI('Finished creating instance: %s') % instance.name)
# Define the flow
flow = lf.Flow("spawn")
# Now start the lpar
power.power_on(self.adapter,
vm.get_instance_wrapper(self.adapter,
instance,
self.pvm_uuids,
self.host_uuid),
self.host_uuid)
# Create the LPAR
flow.add(tf_spawn.tf_crt_lpar(self.adapter, self.host_uuid,
instance, flavor))
# Creates the boot image.
flow.add(tf_spawn.tf_crt_vol_from_img(self.block_dvr,
context,
instance,
image_meta))
# Connects up the volume to the LPAR
flow.add(tf_spawn.tf_connect_vol(self.block_dvr, context, instance))
# Last step is to power on the system.
# Note: If moving to a Graph Flow, will need to change to depend on
# the prior step.
flow.add(tf_spawn.tf_power_on(self.adapter, self.host_uuid, instance))
# Build the engine & run!
engine = taskflow.engines.load(flow)
engine.run()
def destroy(self, context, instance, network_info, block_device_info=None,
destroy_disks=True):

View File

@ -110,11 +110,8 @@ class LocalStorage(blockdev.StorageAdapter):
return {'device_name': vol_name}
def connect_volume(self, context, instance, volume_info, **kwds):
# TODO(IBM): We need the pvm uuid until it's the same as OpenStack
pvm_uuids = kwds['pvm_uuids']
lpar_uuid = pvm_uuids.lookup(instance.name)
def connect_volume(self, context, instance, volume_info, lpar_uuid,
**kwds):
vol_name = volume_info['device_name']
# Create the mapping structure
scsi_map = pvm_vios.crt_scsi_map_to_vdisk(self.adapter, self.host_uuid,

View File

@ -0,0 +1,156 @@
# Copyright 2015 IBM Corp.
#
# All Rights Reserved.
#
# 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 nova.i18n import _LI
from nova.i18n import _LW
from nova.openstack.common import log as logging
from pypowervm.jobs import power
from pypowervm.wrappers import logical_partition as pvm_lpar
from taskflow import task
from nova_powervm.virt.powervm import vm
LOG = logging.getLogger(__name__)
def tf_crt_lpar(adapter, host_uuid, instance, flavor):
"""Creates the Task for the crt_lpar step of the spawn.
Provides the 'lpar_crt_response' for other tasks.
:param adapter: The adapter for the pypowervm API
:param host_uuid: The host UUID
:param instance: The nova instance.
:param flavor: The nova flavor.
"""
def _task(adapter, host_uuid, instance, flavor):
"""Thin wrapper to the crt_lpar in vm."""
LOG.info(_LI('Creating instance: %s') % instance.name)
resp = vm.crt_lpar(adapter, host_uuid, instance, flavor)
return resp
def _revert(adapter, host_uuid, instance, flavor, result, flow_failures):
# The parameters have to match the crt method, plus the response +
# failures even if only a subset are used.
LOG.warn(_LW('Instance %s to be undefined off host') % instance.name)
if result is None or result.entry is None:
# No response, nothing to do
LOG.info(_LI('Instance %s not found on host. No update needed.') %
instance.name)
return
# Wrap to the actual delete.
lpar = pvm_lpar.LogicalPartition(result.entry)
vm.dlt_lpar(adapter, lpar.get_uuid())
LOG.info(_LI('Instance %s removed from system') % instance.name)
return task.FunctorTask(
_task, revert=_revert, name='crt_lpar',
inject={'adapter': adapter, 'host_uuid': host_uuid,
'instance': instance, 'flavor': flavor},
provides='lpar_crt_resp')
def tf_crt_vol_from_img(block_dvr, context, instance, image_meta):
"""Create the Task for the 'create_volume_from_image' in the storage.
Provides the 'vol_dev_info' for other tasks. Comes from the block_dvr
create_volume_from_image method.
:param block_dvr: The storage driver.
:param context: The context passed into the spawn method.
:param instance: The nova instance.
:param image_meta: The image metadata.
"""
def _task(block_dvr, context, instance, image_meta):
LOG.info(_LI('Creating disk for instance: %s') % instance.name)
return block_dvr.create_volume_from_image(context, instance,
image_meta)
def _revert(block_dvr, context, instance, image_meta, result,
flow_failures):
# The parameters have to match the crt method, plus the response +
# failures even if only a subset are used.
LOG.warn(_LW('Image for instance %s to be deleted') % instance.name)
if result is None:
# No result means no volume to clean up.
return
block_dvr.delete_volume(context, result)
return task.FunctorTask(
_task, revert=_revert, name='crt_vol_from_img',
inject={'block_dvr': block_dvr, 'context': context,
'instance': instance, 'image_meta': image_meta},
provides='vol_dev_info')
def tf_connect_vol(block_dvr, context, instance):
"""Create the Task for the connect volume to instance method.
Requires LPAR info through requirement of lpar_crt_resp (provided by
tf_crt_lpar).
Requires volume info through requirement of vol_dev_info (provided by
tf_crt_vol_from_img)
:param block_dvr: The storage driver.
:param context: The context passed into the spawn method.
:param instance: The nova instance.
"""
def _task(block_dvr, context, instance, lpar_crt_resp, vol_dev_info):
LOG.info(_LI('Connecting boot disk to instance: %s') % instance.name)
lpar = pvm_lpar.LogicalPartition(lpar_crt_resp.entry)
block_dvr.connect_volume(context, instance, vol_dev_info,
lpar.get_uuid())
return task.FunctorTask(
_task, name='connect_vol',
inject={'block_dvr': block_dvr, 'context': context,
'instance': instance},
requires=['lpar_crt_resp', 'vol_dev_info'])
def tf_power_on(adapter, host_uuid, instance):
"""Create the Task for the power on of the LPAR.
Obtains LPAR info through requirement of lpar_crt_resp (provided by
tf_crt_lpar).
:param adapter: The pypowervm adapter.
:param host_uuid: The host UUID.
:param instance: The nova instance.
"""
def _task(adapter, host_uuid, instance, lpar_crt_resp):
LOG.info(_LI('Powering on instance: %s') % instance.name)
power.power_on(adapter, host_uuid, lpar_crt_resp.entry)
def _revert(adapter, host_uuid, instance, lpar_crt_resp, result,
flow_failures):
LOG.info(_LI('Powering off instance: %s') % instance.name)
power.power_off(adapter, lpar_crt_resp.entry, host_uuid,
force_immediate=True)
return task.FunctorTask(
_task, revert=_revert, name='pwr_lpar',
inject={'adapter': adapter, 'host_uuid': host_uuid,
'instance': instance},
requires=['lpar_crt_resp'])

View File

@ -204,6 +204,7 @@ def crt_lpar(adapter, host_uuid, instance, flavor):
:param host_uuid: (TEMPORARY) The host UUID
:param instance: The nova instance.
:param flavor: The nova flavor.
:returns: The LPAR response from the API.
"""
mem = str(flavor.memory_mb)
@ -221,8 +222,8 @@ def crt_lpar(adapter, host_uuid, instance, flavor):
max_mem=mem,
max_io_slots='64')
adapter.create(lpar_elem, pvm_consts.MGT_SYS,
root_id=host_uuid, child_type=pvm_lpar.LPAR)
return adapter.create(lpar_elem, pvm_consts.MGT_SYS,
root_id=host_uuid, child_type=pvm_lpar.LPAR)
def dlt_lpar(adapter, lpar_uuid):

View File

@ -4,3 +4,4 @@ six>=1.7.0
oslo.serialization>=1.0.0 # Apache-2.0
oslo.utils>=1.0.0 # Apache-2.0
oslo.config>=1.4.0 # Apache-2.0
taskflow>=0.6