diff --git a/nova_powervm/tests/virt/powervm/test_driver.py b/nova_powervm/tests/virt/powervm/test_driver.py index 6a0b3dfa..32023370 100644 --- a/nova_powervm/tests/virt/powervm/test_driver.py +++ b/nova_powervm/tests/virt/powervm/test_driver.py @@ -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): diff --git a/nova_powervm/virt/powervm/blockdev.py b/nova_powervm/virt/powervm/blockdev.py new file mode 100644 index 00000000..7665a76e --- /dev/null +++ b/nova_powervm/virt/powervm/blockdev.py @@ -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 diff --git a/nova_powervm/virt/powervm/driver.py b/nova_powervm/virt/powervm/driver.py index 8f52cf72..ba3b7ff1 100644 --- a/nova_powervm/virt/powervm/driver.py +++ b/nova_powervm/virt/powervm/driver.py @@ -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. diff --git a/nova_powervm/virt/powervm/localdisk.py b/nova_powervm/virt/powervm/localdisk.py new file mode 100644 index 00000000..787ed9c4 --- /dev/null +++ b/nova_powervm/virt/powervm/localdisk.py @@ -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) diff --git a/nova_powervm/virt/powervm/vios.py b/nova_powervm/virt/powervm/vios.py new file mode 100644 index 00000000..af0b2fb1 --- /dev/null +++ b/nova_powervm/virt/powervm/vios.py @@ -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) diff --git a/nova_powervm/virt/powervm/vm.py b/nova_powervm/virt/powervm/vm.py index 461d26a3..e40bb4a3 100644 --- a/nova_powervm/virt/powervm/vm.py +++ b/nova_powervm/virt/powervm/vm.py @@ -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: