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:
parent
02b8d18b0c
commit
902c2585e1
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
0
nova_powervm/virt/powervm/tasks/__init__.py
Normal file
0
nova_powervm/virt/powervm/tasks/__init__.py
Normal file
156
nova_powervm/virt/powervm/tasks/spawn.py
Normal file
156
nova_powervm/virt/powervm/tasks/spawn.py
Normal 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'])
|
@ -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):
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user