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:
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
@@ -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):
|
||||||
|
@@ -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,
|
||||||
|
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 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):
|
||||||
|
@@ -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
|
Reference in New Issue
Block a user