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 import mock
from nova import exception as exc
from nova import test from nova import test
from nova.virt import fake from nova.virt import fake
from pypowervm.tests.wrappers.util import pvmhttp from pypowervm.tests.wrappers.util import pvmhttp
@@ -135,17 +136,64 @@ class TestPowerVMDriver(test.TestCase):
drv.init_host('FakeHost') drv.init_host('FakeHost')
drv.adapter = mock_apt drv.adapter = mock_apt
# spawn() # Set up the mocks to the tasks.
inst = FakeInstance() inst = FakeInstance()
my_flavor = FakeFlavor() my_flavor = FakeFlavor()
mock_get_flv.return_value = my_flavor mock_get_flv.return_value = my_flavor
mock_crt.return_value = mock.MagicMock()
# Invoke the method.
drv.spawn('context', inst, mock.Mock(), drv.spawn('context', inst, mock.Mock(),
'injected_files', 'admin_password') 'injected_files', 'admin_password')
# Create LPAR was called # Create LPAR was called
mock_crt.assert_called_with(mock_apt, drv.host_uuid, mock_crt.assert_called_with(mock_apt, drv.host_uuid,
inst, my_flavor) inst, my_flavor)
# Power on was called # 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') @mock.patch('nova_powervm.virt.powervm.driver.LOG')
def test_log_op(self, mock_log): def test_log_op(self, mock_log):

View File

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

View File

@@ -23,6 +23,8 @@ from nova.openstack.common import log as logging
from nova.virt import driver from nova.virt import driver
from oslo.config import cfg 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 import adapter as pvm_apt
from pypowervm.helpers import log_helper as log_hlp 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 host as pvm_host
from nova_powervm.virt.powervm import localdisk as blk_lcl 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 vios
from nova_powervm.virt.powervm import vm from nova_powervm.virt.powervm import vm
@@ -159,26 +162,30 @@ class PowerVMDriver(driver.ComputeDriver):
flavor_obj.Flavor.get_by_id(admin_ctx, flavor_obj.Flavor.get_by_id(admin_ctx,
instance.instance_type_id)) instance.instance_type_id))
# Create the lpar on the host # Define the flow
LOG.info(_LI('Creating instance: %s') % instance.name) flow = lf.Flow("spawn")
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)
# Now start the lpar # Create the LPAR
power.power_on(self.adapter, flow.add(tf_spawn.tf_crt_lpar(self.adapter, self.host_uuid,
vm.get_instance_wrapper(self.adapter, instance, flavor))
instance,
self.pvm_uuids, # Creates the boot image.
self.host_uuid), flow.add(tf_spawn.tf_crt_vol_from_img(self.block_dvr,
self.host_uuid) 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, def destroy(self, context, instance, network_info, block_device_info=None,
destroy_disks=True): destroy_disks=True):

View File

@@ -110,11 +110,8 @@ class LocalStorage(blockdev.StorageAdapter):
return {'device_name': vol_name} return {'device_name': vol_name}
def connect_volume(self, context, instance, volume_info, **kwds): def connect_volume(self, context, instance, volume_info, lpar_uuid,
# TODO(IBM): We need the pvm uuid until it's the same as OpenStack **kwds):
pvm_uuids = kwds['pvm_uuids']
lpar_uuid = pvm_uuids.lookup(instance.name)
vol_name = volume_info['device_name'] vol_name = volume_info['device_name']
# Create the mapping structure # Create the mapping structure
scsi_map = pvm_vios.crt_scsi_map_to_vdisk(self.adapter, self.host_uuid, 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 host_uuid: (TEMPORARY) The host UUID
:param instance: The nova instance. :param instance: The nova instance.
:param flavor: The nova flavor. :param flavor: The nova flavor.
:returns: The LPAR response from the API.
""" """
mem = str(flavor.memory_mb) mem = str(flavor.memory_mb)
@@ -221,8 +222,8 @@ def crt_lpar(adapter, host_uuid, instance, flavor):
max_mem=mem, max_mem=mem,
max_io_slots='64') max_io_slots='64')
adapter.create(lpar_elem, pvm_consts.MGT_SYS, return adapter.create(lpar_elem, pvm_consts.MGT_SYS,
root_id=host_uuid, child_type=pvm_lpar.LPAR) root_id=host_uuid, child_type=pvm_lpar.LPAR)
def dlt_lpar(adapter, lpar_uuid): 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.serialization>=1.0.0 # Apache-2.0
oslo.utils>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0
oslo.config>=1.4.0 # Apache-2.0 oslo.config>=1.4.0 # Apache-2.0
taskflow>=0.6