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:
parent
954d14f008
commit
37e76fe8ec
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue