Transfer image and connect to instance on spawn

Add code to transfer the image to vios and connect it to the LPAR.
Add a block device adapter interface.
Correct exception handling in cache lookup and other locations.
Ignore lpar not found exception on destroy.

Change-Id: Id3d179a8d407be4dacf50d5baa712e9b87570230
This commit is contained in:
Kyle L. Henderson 2015-01-14 09:21:17 -06:00
parent 954d14f008
commit 37e76fe8ec
6 changed files with 379 additions and 12 deletions

View File

@ -79,7 +79,8 @@ class TestPowerVMDriver(test.TestCase):
@mock.patch('pypowervm.adapter.Session')
@mock.patch('pypowervm.adapter.Adapter')
@mock.patch('nova_powervm.virt.powervm.host.find_entry_by_mtm_serial')
def test_driver_init(self, mock_find, mock_apt, mock_sess):
@mock.patch('nova_powervm.virt.powervm.localdisk.LocalStorage')
def test_driver_init(self, mock_disk, mock_find, mock_apt, mock_sess):
"""Validates the PowerVM driver can be initialized for the host."""
drv = driver.PowerVMDriver(fake.FakeVirtAPI())
drv.init_host('FakeHost')
@ -92,7 +93,8 @@ class TestPowerVMDriver(test.TestCase):
@mock.patch('nova_powervm.virt.powervm.vm.UUIDCache')
@mock.patch('nova.context.get_admin_context')
@mock.patch('nova.objects.flavor.Flavor.get_by_id')
def test_driver_ops(self, mock_get_flv, mock_get_ctx,
@mock.patch('nova_powervm.virt.powervm.localdisk.LocalStorage')
def test_driver_ops(self, mock_disk, mock_get_flv, mock_get_ctx,
mock_uuidcache, mock_find,
mock_apt, mock_sess):
"""Validates the PowerVM driver operations."""
@ -115,14 +117,35 @@ class TestPowerVMDriver(test.TestCase):
inst_list = drv.list_instances()
self.assertEqual(fake_lpar_list, inst_list)
@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.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')
def test_spawn_ops(self, mock_pwron, mock_disk, mock_get_flv, mock_get_ctx,
mock_uuidcache, mock_crt, mock_find, mock_apt,
mock_sess):
"""Validates the PowerVM driver operations."""
drv = driver.PowerVMDriver(fake.FakeVirtAPI())
drv.init_host('FakeHost')
drv.adapter = mock_apt
# spawn()
with mock.patch('nova_powervm.virt.powervm.vm.crt_lpar') as mock_crt:
my_flavor = FakeFlavor()
mock_get_flv.return_value = my_flavor
drv.spawn('context', inst, 'image_meta',
'injected_files', 'admin_password')
mock_crt.assert_called_with(mock_apt, drv.host_uuid,
inst, my_flavor)
inst = FakeInstance()
my_flavor = FakeFlavor()
mock_get_flv.return_value = my_flavor
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)
@mock.patch('nova_powervm.virt.powervm.driver.LOG')
def test_log_op(self, mock_log):

View File

@ -0,0 +1,52 @@
# 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.
import abc
import six
@six.add_metaclass(abc.ABCMeta)
class StorageAdapter(object):
def __init__(self, connection):
"""Initialize the DiskAdapter
:param connection: connection information for the underlying driver
"""
self._connection = connection
def delete_volume(self, context, volume_info):
"""Removes the disk and its associated connection
:param context: nova context for operation
:param volume_info: dictionary with volume info
"""
pass
def create_volume_from_image(self, context, instance, image):
"""Creates a Volume and copies the specified image to it
:param context: nova context used to retrieve image from glance
:param instance: instance to create the volume for
:param image_id: image_id reference used to locate image in glance
:returns: dictionary with the name of the created
disk device in 'device_name' key
"""
pass
def connect_volume(self, context, instance, volume, **kwds):
pass

View File

@ -16,6 +16,7 @@
from nova import context as ctx
from nova import exception
from nova.i18n import _LI
from nova.objects import flavor as flavor_obj
from nova.openstack.common import log as logging
@ -31,6 +32,8 @@ from pypowervm.wrappers import constants as pvm_consts
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 import vios
from nova_powervm.virt.powervm import vm
LOG = logging.getLogger(__name__)
@ -59,6 +62,9 @@ class PowerVMDriver(driver.ComputeDriver):
self._get_host_uuid()
# Initialize the UUID Cache. Lets not prime it at this time.
self.pvm_uuids = vm.UUIDCache(self.adapter)
self._get_vios_uuid()
# Initialize the disk adapter
self._get_blockdev_driver()
LOG.info(_LI("The compute driver has been initialized."))
def _get_adapter(self):
@ -70,6 +76,18 @@ class PowerVMDriver(driver.ComputeDriver):
self.adapter = pvm_apt.Adapter(self.session,
helpers=log_hlp.log_helper)
def _get_blockdev_driver(self):
# TODO(IBM): load driver from conf
conn_info = {'adapter': self.adapter,
'host_uuid': self.host_uuid,
'vios_name': CONF.vios_name,
'vios_uuid': self.vios_uuid}
self.block_dvr = blk_lcl.LocalStorage(conn_info)
def _get_vios_uuid(self):
self.vios_uuid = vios.get_vios_uuid(self.adapter, CONF.vios_name)
LOG.info(_LI("VIOS UUID is:%s") % self.vios_uuid)
def _get_host_uuid(self):
# Need to get a list of the hosts, then find the matching one
resp = self.adapter.read(pvm_consts.MGT_SYS)
@ -151,7 +169,25 @@ class PowerVMDriver(driver.ComputeDriver):
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)
# 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)
def destroy(self, context, instance, network_info, block_device_info=None,
destroy_disks=True):
@ -177,7 +213,12 @@ class PowerVMDriver(driver.ComputeDriver):
self._fake.destroy(instance, network_info,
block_device_info, destroy_disks)
else:
vm.dlt_lpar(self.adapter, self.pvm_uuids.lookup(instance.name))
try:
vm.dlt_lpar(self.adapter, self.pvm_uuids.lookup(instance.name))
# TODO(IBM): delete the disk and connections
except exception.InstanceNotFound:
# Don't worry if the instance wasn't found
pass
return
def attach_volume(self, connection_info, instance, mountpoint):
@ -259,12 +300,12 @@ class PowerVMDriver(driver.ComputeDriver):
def plug_vifs(self, instance, network_info):
"""Plug VIFs into networks."""
self._log_operation('plug_vifs', instance)
pass
# TODO(IBM): Implement
def unplug_vifs(self, instance, network_info):
"""Unplug VIFs from networks."""
self._log_operation('unplug_vifs', instance)
pass
# TODO(IBM): Implement
def get_available_nodes(self):
"""Returns nodenames of all nodes managed by the compute service.

View File

@ -0,0 +1,147 @@
# Copyright 2013 OpenStack Foundation
# 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.
import abc
from oslo.config import cfg
import six
from nova import image
from nova.i18n import _LI, _LE
from nova.openstack.common import log as logging
from pypowervm.jobs import upload_lv
from pypowervm.wrappers import constants as pvm_consts
from pypowervm.wrappers import virtual_io_server as pvm_vios
from pypowervm.wrappers import volume_group as vol_grp
from nova_powervm.virt.powervm import blockdev
from nova_powervm.virt.powervm import vios
localdisk_opts = [
cfg.StrOpt('volume_group_name',
default='',
help='Volume Group to use for block device operations.')
]
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.register_opts(localdisk_opts)
IMAGE_API = image.API()
@six.add_metaclass(abc.ABCMeta)
class AbstractLocalStorageException(Exception):
def __init__(self, **kwds):
msg = self.msg_fmt % kwds
super(AbstractLocalStorageException, self).__init__(msg)
class VGNotFound(AbstractLocalStorageException):
msg_fmt = _LE('Unable to locate the volume group \'%(vg_name)s\''
' for this operation.')
class IterableToFileAdapter(object):
"""A degenerate file-like so that an iterable could be read like a file.
As Glance client returns an iterable, but PowerVM requires a file,
this is the adapter between the two.
Taken from xenapi/image/apis.py
"""
def __init__(self, iterable):
self.iterator = iterable.__iter__()
self.remaining_data = ''
def read(self, size):
chunk = self.remaining_data
try:
while not chunk:
chunk = self.iterator.next()
except StopIteration:
return ''
return_value = chunk[0:size]
self.remaining_data = chunk[size:]
return return_value
class LocalStorage(blockdev.StorageAdapter):
def __init__(self, connection):
super(LocalStorage, self).__init__(connection)
self.adapter = connection['adapter']
self.host_uuid = connection['host_uuid']
self.vios_name = connection['vios_name']
self.vios_uuid = connection['vios_uuid']
self.vg_name = CONF.volume_group_name
self.vg_uuid = self._get_vg_uuid(self.adapter, self.vios_uuid,
CONF.volume_group_name)
LOG.info(_LI('Local Storage driver initialized: '
'volume group: \'%s\'') % self.vg_name)
def delete_volume(self, context, volume_info):
# TODO(IBM):
pass
def create_volume_from_image(self, context, instance, image):
LOG.info(_LI('Create volume.'))
# Transfer the image
chunks = IMAGE_API.download(context, image['id'])
stream = IterableToFileAdapter(chunks)
vol_name = self._get_disk_name('boot', instance)
upload_lv.upload_new_vdisk(self.adapter, self.vios_uuid, self.vg_uuid,
stream, vol_name, image['size'])
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)
vol_name = volume_info['device_name']
# Create the mapping structure
scsi_map = pvm_vios.crt_scsi_map_to_vdisk(self.adapter, self.host_uuid,
lpar_uuid, vol_name)
# Add the mapping to the VIOS
vios.add_vscsi_mapping(self.adapter, self.vios_uuid, self.vios_name,
scsi_map)
def _get_disk_name(self, type_, instance):
return type_[:6] + '_' + instance.uuid[:8]
def _get_vg_uuid(self, adapter, vios_uuid, name):
try:
resp = adapter.read(pvm_consts.VIOS,
rootId=vios_uuid,
childType=pvm_consts.VOL_GROUP)
except Exception as e:
LOG.exception(e)
raise e
# Search the feed for the volume group
for entry in resp.feed.entries:
wrapper = vol_grp.VolumeGroup(entry)
wrap_vg_name = wrapper.get_name()
LOG.info(_LI('Volume group: %s') % wrap_vg_name)
if name == wrap_vg_name:
uuid = entry.properties['id']
return uuid
raise VGNotFound(vg_name=name)

View File

@ -0,0 +1,101 @@
# 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.
import abc
from oslo.config import cfg
import six
from nova.i18n import _LE
from nova.openstack.common import log as logging
from pypowervm import exceptions as pvm_exc
from pypowervm.wrappers import constants as pvm_consts
from pypowervm.wrappers import virtual_io_server as pvm_vios
vios_opts = [
cfg.StrOpt('vios_name',
default='',
help='VIOS to use for I/O operations.')
]
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.register_opts(vios_opts)
@six.add_metaclass(abc.ABCMeta)
class AbstractVIOSException(Exception):
def __init__(self, **kwds):
msg = self.msg_fmt % kwds
super(AbstractVIOSException, self).__init__(msg)
class VIOSNotFound(AbstractVIOSException):
msg_fmt = _LE('Unable to locate the Virtual I/O Server \'%(vios_name)s\''
' for this operation.')
def get_vios_uuid(adapter, name):
searchstring = "(PartitionName=='%s')" % name
try:
resp = adapter.read(pvm_consts.VIOS,
suffixType='search',
suffixParm=searchstring)
except pvm_exc.Error as e:
if e.response.status == 404:
raise VIOSNotFound(vios_name=name)
else:
LOG.exception(e)
raise
except Exception as e:
LOG.exception(e)
raise
entry = resp.feed.entries[0]
uuid = entry.properties['id']
return uuid
def get_vios_entry(adapter, vios_uuid, vios_name):
try:
resp = adapter.read(pvm_consts.VIOS, rootId=vios_uuid)
except pvm_exc.Error as e:
if e.response.status == 404:
raise VIOSNotFound(vios_name=vios_name)
else:
LOG.exception(e)
raise
except Exception as e:
LOG.exception(e)
raise
return resp.entry, resp.headers['etag']
def add_vscsi_mapping(adapter, vios_uuid, vios_name, scsi_map):
# Get the VIOS Entry
vios_entry, etag = get_vios_entry(adapter, vios_uuid, vios_name)
# Wrap the entry
vios_wrap = pvm_vios.VirtualIOServer(vios_entry)
# Pull the current mappings
cur_mappings = vios_wrap.get_scsi_mappings()
# Add the new mapping to the end
cur_mappings.append(pvm_vios.VirtualSCSIMapping(scsi_map))
vios_wrap.set_scsi_mappings(cur_mappings)
# Write it back to the VIOS
adapter.update(vios_wrap._entry.element, etag,
pvm_consts.VIOS, vios_uuid, xag=None)

View File

@ -101,6 +101,7 @@ class InstanceInfo(hardware.InstanceInfo):
raise exception.InstanceNotFound(instance_id=self._name)
else:
LOG.exception(e)
raise
return resp.body.lstrip(' "').rstrip(' "')
@ -261,8 +262,10 @@ class UUIDCache(object):
raise exception.InstanceNotFound(instance_id=name)
else:
LOG.exception(e)
raise
except Exception as e:
LOG.exception(e)
raise
# Process the response
if len(resp.feed.entries) == 0: